Преглед на файлове

feat(ScenarioSelector): adjust and improve styles (#138)

* feat(ScenarioSelector): adjust styles

* feat(icons): add taipycons

* feat(ScenarioSelector): adjust tree item style

* feat(ScenarioSelector): add background totree item

* feat(ScenarioSelector): Improve scycle item styles

* feat(ScenarioSelector): improve dialogs styles

* fix(ScenarioSelector): exteralize sx props

* fix(icons): rename icons

* fix(icons): export icon components consistantly

* minor changes

---------

Co-authored-by: Jaufrey Lafuste <jaufrey.lafuste@winzana.com>
Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Jaufrey Lafuste преди 2 години
родител
ревизия
41469f5724

+ 22 - 0
.editorconfig

@@ -0,0 +1,22 @@
+# http://editorconfig.org
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+max_line_length = 120
+trim_trailing_whitespace = true
+insert_final_newline = true
+charset = utf-8
+end_of_line = lf
+
+[*.bat]
+indent_style = tab
+end_of_line = crlf
+
+[LICENSE]
+insert_final_newline = false
+
+[Makefile]
+indent_style = tab

+ 614 - 428
gui/src/ScenarioSelector.tsx

@@ -12,6 +12,7 @@
  */
 
 import React, { useEffect, useState, useCallback } from "react";
+import { alpha, useTheme } from "@mui/material";
 import Badge, { BadgeOrigin } from "@mui/material/Badge";
 import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
@@ -35,27 +36,28 @@ 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 {
-  useDynamicProperty,
-  useDispatch,
-  useModule,
-  createRequestUpdateAction,
-  getUpdateVar,
-  createSendActionNameAction,
-  useDispatchRequestUpdateOnFirstRender,
-  createSendUpdateAction,
+    useDynamicProperty,
+    useDispatch,
+    useModule,
+    createRequestUpdateAction,
+    getUpdateVar,
+    createSendActionNameAction,
+    useDispatchRequestUpdateOnFirstRender,
+    createSendUpdateAction,
 } from "taipy-gui";
 
+import { Cycle, Scenario } from "./icons";
+
 enum NodeType {
-  CYCLE = 0,
-  SCENARIO = 1,
+    CYCLE = 0,
+    SCENARIO = 1,
 }
 
 type Property = {
-  id: string;
-  key: string;
-  value: string;
+    id: string;
+    key: string;
+    value: string;
 };
 
 type Scenario = [string, string, undefined, number, boolean];
@@ -63,478 +65,662 @@ type Scenarios = Array<Scenario>;
 type Cycles = Array<[string, string, Scenarios, number, boolean]>;
 
 // id, is_primary, config_id, creation_date, label, tags, properties(key, value), pipelines(id, label), authorized_tags
-type ScenarioFull = [string, boolean, string, string, string, string[], Array<[string, string]>, Array<[string, string]>, string[]];
+type ScenarioFull = [
+    string,
+    boolean,
+    string,
+    string,
+    string,
+    string[],
+    Array<[string, string]>,
+    Array<[string, string]>,
+    string[]
+];
 interface ScenarioDict {
-  config: string;
-  name: string;
-  date: string;
-  properties: Array<[string, string]>;
+    config: string;
+    name: string;
+    date: string;
+    properties: Array<[string, string]>;
 }
 
 interface ScenarioSelectorProps {
-  id?: string;
-  defaultShowAddButton: boolean;
-  showAddButton?: boolean;
-  defaultDisplayCycles: boolean;
-  displayCycles?: boolean;
-  defaultShowPrimaryFlag: boolean;
-  showPrimaryFlag?: boolean;
-  value?: Record<string, any>;
-  updateVarName?: string;
-  scenarios?: Cycles | Scenarios;
-  onScenarioCrud: string;
-  onChange?: string;
-  coreChanged?: Record<string, unknown>;
-  updateVars: string;
-  configs?: Array<[string, string]>;
-  error?: string;
-  propagate?: boolean;
-  scenarioEdit?: ScenarioFull;
-  onScenarioSelect: string;
+    id?: string;
+    defaultShowAddButton: boolean;
+    showAddButton?: boolean;
+    defaultDisplayCycles: boolean;
+    displayCycles?: boolean;
+    defaultShowPrimaryFlag: boolean;
+    showPrimaryFlag?: boolean;
+    value?: Record<string, any>;
+    updateVarName?: string;
+    scenarios?: Cycles | Scenarios;
+    onScenarioCrud: string;
+    onChange?: string;
+    coreChanged?: Record<string, unknown>;
+    updateVars: string;
+    configs?: Array<[string, string]>;
+    error?: string;
+    propagate?: boolean;
+    scenarioEdit?: ScenarioFull;
+    onScenarioSelect: string;
 }
 
 interface ScenarioNodesProps {
-  scenarios?: Scenarios | Scenario;
-  showPrimary?: boolean;
-  openEditDialog: (e: React.MouseEvent<HTMLElement>) => void;
+    scenarios?: Scenarios | Scenario;
+    showPrimary?: boolean;
+    openEditDialog: (e: React.MouseEvent<HTMLElement>) => void;
 }
 
-interface ScenarioNodesContentProps {
-  scenarioId?: string;
-  label?: string;
-  openEditDialog: (e: React.MouseEvent<HTMLElement>) => void;
+interface ScenarioItemProps {
+    scenarioId?: string;
+    label?: string;
+    isPrimary?: boolean;
+    openEditDialog: (e: React.MouseEvent<HTMLElement>) => void;
 }
 
 interface ScenarioEditDialogProps {
-  scenario?: ScenarioFull;
-  submit: (...values: any[]) => void;
-  open: boolean;
-  actionEdit: boolean;
-  configs?: Array<[string, string]>;
-  close: () => void;
+    scenario?: ScenarioFull;
+    submit: (...values: any[]) => void;
+    open: boolean;
+    actionEdit: boolean;
+    configs?: Array<[string, string]>;
+    close: () => void;
 }
 
 const emptyScenario: ScenarioDict = {
-  config: "",
-  name: "",
-  date: new Date().toISOString(),
-  properties: [],
+    config: "",
+    name: "",
+    date: new Date().toISOString(),
+    properties: [],
 };
 
 const BadgePos = {
-  vertical: "top",
-  horizontal: "left",
+    vertical: "top",
+    horizontal: "left",
 } as BadgeOrigin;
 
 const BadgeSx = {
-  "& .MuiBadge-badge": {
-    marginLeft: "-12px",
-    height: "19px",
-    width: "12px",
-  },
-  width: "100%",
+    flex: "0 0 auto",
+
+    "& .MuiBadge-badge": {
+        fontSize: "1rem",
+        width: "1em",
+        height: "1em",
+        p: 0,
+        minWidth: "0",
+    },
 };
 
 const FlagSx = {
-  color: "#FFFFFF",
-  fontSize: "11px",
+    color: "common.white",
+    fontSize: "0.75em",
+};
+
+const tinyIconButtonSx = {
+    position: "relative",
+    display: "flex",
+    width: "1rem",
+    height: "1rem",
+    fontSize: "0.750rem",
+
+    "&::before": {
+        content: "''",
+        position: "absolute",
+        top: "50%",
+        left: "50%",
+        transform: "translate(-50%,-50%)",
+        width: "2rem",
+        height: "2rem",
+    },
+
+    "& .MuiSvgIcon-root": {
+        color: "inherit",
+        fontSize: "inherit",
+    },
 };
 
 const ActionContentSx = { mr: 2, ml: 2 };
 
 const MainBoxSx = {
-  maxWidth: 300,
-  overflowY: "auto",
+    maxWidth: 300,
+    overflowY: "auto",
 };
 
 const TreeViewSx = {
-  mb: 2,
+    mb: 2,
+
+    "& .MuiTreeItem-root .MuiTreeItem-content": {
+        mb: 0.5,
+        py: 1,
+        px: 2,
+        borderRadius: 0.5,
+        backgroundColor: "background.paper",
+    },
+
+    "& .MuiTreeItem-iconContainer:empty": {
+        display: "none",
+    },
+};
+
+const treeItemLabelSx = {
+    display: "flex",
+    alignItems: "center",
+    gap: 1,
 };
 
 const CycleSx = {
-  ".MuiTreeItem-content": {
-    padding: "4px 8px",
-    gap: "4px",
-    borderRadius: "4px",
-    mb: "5px",
-  },
-  ".MuiTreeItem-label": {
-    fontWeight: "700",
-    fontSize: "16px",
-  },
+    "& > .MuiTreeItem-content": {
+        ".MuiTreeItem-label": {
+            fontWeight: "fontWeightBold",
+        },
+    },
 };
 
 const DialogContentSx = {
-  width: "500px",
-  maxHeight: "calc(100vh - 256px)",
+    maxHeight: "calc(100vh - 256px)",
+
+    "& .MuiFormControl-root": {
+        maxWidth: "100%",
+    },
+};
+
+const configHelperTextSx = { pl: 12 };
+
+const SquareButtonSx = {
+    mb: 0,
+    p: 0,
+    minWidth: 0,
+    aspectRatio: "1",
 };
 
 const CancelBtnSx = {
-  mr: 2,
+    mr: 2,
 };
 
 const IconButtonSx = {
-  p: 0,
+    p: 0,
 };
 
-const ScenarioNodesContent = ({ scenarioId, label, openEditDialog }: ScenarioNodesContentProps) => {
-  return (
-    <Grid container alignItems="center" direction="row" flexWrap="nowrap" justifyContent="space-between" spacing={1}>
-      <Grid item>{label}</Grid>
-      <Grid item>
-        <IconButton data-id={scenarioId} onClick={openEditDialog}>
-          <EditOutlined fontSize="small" color="primary" />
-        </IconButton>
-      </Grid>
-    </Grid>
-  );
+const ScenarioItem = ({ scenarioId, label, isPrimary, openEditDialog }: ScenarioItemProps) => {
+    const theme = useTheme();
+
+    const tinyEditIconButtonSx = React.useMemo(
+        () => ({
+            ...tinyIconButtonSx,
+            backgroundColor: alpha(theme.palette.text.primary, 0.15),
+            color: "text.primary",
+
+            "&:hover": {
+                backgroundColor: "primary.main",
+                color: "primary.contrastText",
+            },
+        }),
+        [theme]
+    );
+
+    return (
+        <Grid container alignItems="center" direction="row" flexWrap="nowrap" spacing={1}>
+            <Grid item xs sx={treeItemLabelSx}>
+                {isPrimary ? (
+                    <Badge
+                        badgeContent={<FlagOutlined sx={FlagSx} />}
+                        color="primary"
+                        anchorOrigin={BadgePos}
+                        sx={BadgeSx}
+                    >
+                        <Scenario fontSize="small" color="primary" />
+                    </Badge>
+                ) : (
+                    <Scenario fontSize="small" color="primary" />
+                )}
+                {label}
+            </Grid>
+            <Grid item xs="auto">
+                <IconButton data-id={scenarioId} onClick={openEditDialog} sx={tinyEditIconButtonSx}>
+                    <EditOutlined />
+                </IconButton>
+            </Grid>
+        </Grid>
+    );
 };
 
 const ScenarioNodes = ({ scenarios = [], showPrimary = true, openEditDialog }: ScenarioNodesProps) => {
-  const sc = Array.isArray(scenarios) && scenarios.length && Array.isArray(scenarios[0]) ? (scenarios as Scenarios) : scenarios ? [scenarios as Scenario] : [];
-  return (
-    <>
-      {sc.map(([id, label, _, _nodeType, primary]) => (
-        <TreeItem
-          key={id}
-          nodeId={id}
-          label={
-            showPrimary && primary ? (
-              <Badge badgeContent={<FlagOutlined sx={FlagSx} />} color="primary" anchorOrigin={BadgePos} sx={BadgeSx}>
-                <ScenarioNodesContent scenarioId={id} label={label} openEditDialog={openEditDialog} />
-              </Badge>
-            ) : (
-              <ScenarioNodesContent scenarioId={id} label={label} openEditDialog={openEditDialog} />
-            )
-          }
-        />
-      ))}
-    </>
-  );
+    const sc =
+        Array.isArray(scenarios) && scenarios.length && Array.isArray(scenarios[0])
+            ? (scenarios as Scenarios)
+            : scenarios
+            ? [scenarios as Scenario]
+            : [];
+    return (
+        <>
+            {sc.map(([id, label, _, _nodeType, primary]) => (
+                <TreeItem
+                    key={id}
+                    nodeId={id}
+                    label={
+                        <ScenarioItem
+                            scenarioId={id}
+                            label={label}
+                            isPrimary={showPrimary && primary}
+                            openEditDialog={openEditDialog}
+                        />
+                    }
+                />
+            ))}
+        </>
+    );
 };
 
 const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close }: ScenarioEditDialogProps) => {
-  const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
-  const [properties, setProperties] = useState<Property[]>([]);
-  const [newProp, setNewProp] = useState<Property>({
-    id: "",
-    key: "",
-    value: "",
-  });
-
-  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;
+    const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
+    const [properties, setProperties] = useState<Property[]>([]);
+    const [newProp, setNewProp] = useState<Property>({
+        id: "",
+        key: "",
+        value: "",
+    });
+
+    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 }));
             }
-            return p;
-          })
+        }
+    }, []);
+
+    useEffect(() => {
+        form.setValues(
+            scenario
+                ? {
+                      config: scenario[2],
+                      name: scenario[4],
+                      date: scenario[3],
+                      properties: scenario[6],
+                  }
+                : emptyScenario
         );
-      } else {
-        setNewProp((np) => ({ ...np, [name]: e.target.value }));
-      }
-    }
-  }, []);
-
-  useEffect(() => {
-    form.setValues(scenario ? { config: scenario[2], name: scenario[4], date: scenario[3], properties: scenario[6] } : emptyScenario);
-    setProperties(scenario ? scenario[6].map(([k, v], i) => ({ id: i + "", key: k, value: v })) : []);
-  }, [scenario]);
-
-  const form = useFormik({
-    initialValues: emptyScenario,
-    onSubmit: (values: any) => {
-      values.properties = [...properties];
-      actionEdit && scenario && (values.id = scenario[0]);
-      setProperties([]);
-      submit(actionEdit, false, values);
-      form.resetForm();
-      close();
-    },
-  });
-
-  const onDeleteScenario = useCallback(() => {
-    submit(actionEdit, true, { id: scenario && scenario[0] });
-    setConfirmDialogOpen(false);
-    close();
-  }, [close, actionEdit, scenario]);
-
-  const onConfirmDialogOpen = useCallback(() => setConfirmDialogOpen(true), []);
-
-  const onConfirmDialogClose = useCallback(() => setConfirmDialogOpen(false), []);
-
-  return (
-    <>
-      <Dialog onClose={close} open={open}>
-        <DialogTitle>
-          <Grid container direction="row" justifyContent="space-between" alignItems="center">
-            <Typography variant="h5">{`${actionEdit ? `Edit` : `Create`} scenario`}</Typography>
-            <IconButton aria-label="close" onClick={close} sx={IconButtonSx}>
-              <Close />
-            </IconButton>
-          </Grid>
-        </DialogTitle>
-        <form onSubmit={form.handleSubmit}>
-          <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}
-                      disabled={actionEdit}
-                    >
-                      {configs
-                        ? configs.map(([id, label]) => (
-                            <MenuItem key={id} value={id}>
-                              {label}
-                            </MenuItem>
-                          ))
-                        : null}
-                    </Select>
-                    <FormHelperText error={!!form.errors.config && form.touched.config} sx={{ pl: 12 }}>
-                      {form.errors.config}
-                    </FormHelperText>
-                  </FormControl>
-                </FormGroup>
-              </Grid>
-              <Grid item xs={12}>
-                <FormGroup>
-                  <TextField
-                    {...form.getFieldProps("name")}
-                    error={!!form.errors.name && form.touched.name}
-                    helperText={form.errors.name}
-                    label="Label"
-                    variant="outlined"
-                  />
-                </FormGroup>
-              </Grid>
-              <Grid item xs={12}>
-                <FormGroup>
-                  <LocalizationProvider dateAdapter={AdapterDateFns}>
-                    <DatePicker
-                      label="Date"
-                      value={new Date(form.values.date)}
-                      onChange={(date) => form.setFieldValue("date", date?.toISOString())}
-                      disabled={actionEdit}
-                    />
-                  </LocalizationProvider>
-                </FormGroup>
-              </Grid>
-              <Grid item xs={12} container justifyContent="space-between">
-                <Typography variant="h6">Custom Properties</Typography>
-              </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>
+        setProperties(scenario ? scenario[6].map(([k, v], i) => ({ id: i + "", key: k, value: v })) : []);
+    }, [scenario]);
+
+    const form = useFormik({
+        initialValues: emptyScenario,
+        onSubmit: (values: any) => {
+            values.properties = [...properties];
+            actionEdit && scenario && (values.id = scenario[0]);
+            setProperties([]);
+            submit(actionEdit, false, values);
+            form.resetForm();
+            close();
+        },
+    });
+
+    const onDeleteScenario = useCallback(() => {
+        submit(actionEdit, true, { id: scenario && scenario[0] });
+        setConfirmDialogOpen(false);
+        close();
+    }, [close, actionEdit, scenario]);
+
+    const onConfirmDialogOpen = useCallback(() => setConfirmDialogOpen(true), []);
+
+    const onConfirmDialogClose = useCallback(() => setConfirmDialogOpen(false), []);
+
+    return (
+        <>
+            <Dialog onClose={close} open={open} maxWidth="sm">
+                <DialogTitle>
+                    <Grid container direction="row" justifyContent="space-between" alignItems="center">
+                        <Typography variant="h5">{`${actionEdit ? `Edit` : `Create`} scenario`}</Typography>
+                        <IconButton aria-label="close" onClick={close} sx={IconButtonSx}>
+                            <Close />
+                        </IconButton>
                     </Grid>
-                  ))
-                : null}
-              <Grid item xs={12} container justifyContent="space-between">
-                <Grid item xs={4}>
-                  <TextField value={newProp.key} data-name="key" onChange={updatePropertyField} label="Key" variant="outlined" />
-                </Grid>
-                <Grid item xs={5}>
-                  <TextField value={newProp.value} data-name="value" onChange={updatePropertyField} label="Value" variant="outlined" />
-                </Grid>
-                <Grid item xs={2}>
-                  <Button variant="outlined" component="label" onClick={propertyAdd} disabled={!newProp.key || !newProp.value}>
-                    <Add />
-                  </Button>
-                </Grid>
-              </Grid>
-            </Grid>
-          </DialogContent>
-
-          <DialogActions>
-            <Grid container justifyContent="space-between" sx={ActionContentSx}>
-              {actionEdit && (
-                <Grid item xs={6}>
-                  <Button variant="outlined" color="primary" onClick={onConfirmDialogOpen}>
-                    DELETE
-                  </Button>
-                </Grid>
-              )}
-              <Grid item container xs={actionEdit ? 6 : 12} justifyContent="flex-end">
-                <Grid item sx={CancelBtnSx}>
-                  <Button variant="outlined" onClick={close}>
-                    CANCEL
-                  </Button>
-                </Grid>
-                <Grid item>
-                  <Button variant="contained" type="submit" disabled={!form.values.config || !form.values.name}>
-                    {actionEdit ? "APPLY" : "CREATE"}
-                  </Button>
-                </Grid>
-              </Grid>
-            </Grid>
-          </DialogActions>
-        </form>
-      </Dialog>
-
-      <Dialog onClose={onConfirmDialogClose} open={confirmDialogOpen}>
-        <DialogTitle>
-          <Grid container direction="row" justifyContent="space-between" alignItems="center">
-            <Typography variant="h5">Delete Scenario</Typography>
-            <IconButton aria-label="close" onClick={onConfirmDialogClose} sx={IconButtonSx}>
-              <Close />
-            </IconButton>
-          </Grid>
-        </DialogTitle>
-        <DialogContent dividers>
-          <Typography>Are you sure you want to delete this scenario?</Typography>
-        </DialogContent>
-
-        <DialogActions>
-          <Button variant="outlined" color="inherit" onClick={onConfirmDialogClose}>
-            CANCEL
-          </Button>
-          <Button variant="contained" color="primary" onClick={onDeleteScenario}>
-            DELETE
-          </Button>
-        </DialogActions>
-      </Dialog>
-    </>
-  );
+                </DialogTitle>
+                <form onSubmit={form.handleSubmit}>
+                    <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}
+                                            disabled={actionEdit}
+                                        >
+                                            {configs
+                                                ? configs.map(([id, label]) => (
+                                                      <MenuItem key={id} value={id}>
+                                                          {label}
+                                                      </MenuItem>
+                                                  ))
+                                                : null}
+                                        </Select>
+                                        <FormHelperText
+                                            error={!!form.errors.config && form.touched.config}
+                                            sx={configHelperTextSx}
+                                        >
+                                            {form.errors.config}
+                                        </FormHelperText>
+                                    </FormControl>
+                                </FormGroup>
+                            </Grid>
+                            <Grid item xs={12}>
+                                <FormGroup>
+                                    <TextField
+                                        {...form.getFieldProps("name")}
+                                        error={!!form.errors.name && form.touched.name}
+                                        helperText={form.errors.name}
+                                        label="Label"
+                                        variant="outlined"
+                                    />
+                                </FormGroup>
+                            </Grid>
+                            <Grid item xs={12}>
+                                <FormGroup>
+                                    <LocalizationProvider dateAdapter={AdapterDateFns}>
+                                        <DatePicker
+                                            label="Date"
+                                            value={new Date(form.values.date)}
+                                            onChange={(date) => form.setFieldValue("date", date?.toISOString())}
+                                            disabled={actionEdit}
+                                        />
+                                    </LocalizationProvider>
+                                </FormGroup>
+                            </Grid>
+                            <Grid item xs={12} container justifyContent="space-between">
+                                <Typography variant="h6">Custom Properties</Typography>
+                            </Grid>
+                            {properties
+                                ? properties.map((item, index) => (
+                                      <Grid item xs={12} key={item.id} container spacing={1} alignItems="center">
+                                          <Grid item xs={4}>
+                                              <TextField
+                                                  value={item.key}
+                                                  label="Key"
+                                                  variant="outlined"
+                                                  data-name="key"
+                                                  data-idx={index}
+                                                  onChange={updatePropertyField}
+                                              />
+                                          </Grid>
+                                          <Grid item xs>
+                                              <TextField
+                                                  value={item.value}
+                                                  label="Value"
+                                                  variant="outlined"
+                                                  data-name="value"
+                                                  data-idx={index}
+                                                  onChange={updatePropertyField}
+                                              />
+                                          </Grid>
+                                          <Grid item xs="auto">
+                                              <Button
+                                                  variant="outlined"
+                                                  color="inherit"
+                                                  data-id={item.id}
+                                                  onClick={propertyDelete}
+                                                  sx={SquareButtonSx}
+                                              >
+                                                  <DeleteOutline />
+                                              </Button>
+                                          </Grid>
+                                      </Grid>
+                                  ))
+                                : null}
+                            <Grid item xs={12} container spacing={1} justifyContent="space-between">
+                                <Grid item xs={4}>
+                                    <TextField
+                                        value={newProp.key}
+                                        data-name="key"
+                                        onChange={updatePropertyField}
+                                        label="Key"
+                                        variant="outlined"
+                                    />
+                                </Grid>
+                                <Grid item xs>
+                                    <TextField
+                                        value={newProp.value}
+                                        data-name="value"
+                                        onChange={updatePropertyField}
+                                        label="Value"
+                                        variant="outlined"
+                                    />
+                                </Grid>
+                                <Grid item xs="auto">
+                                    <Button
+                                        variant="outlined"
+                                        onClick={propertyAdd}
+                                        disabled={!newProp.key || !newProp.value}
+                                        sx={SquareButtonSx}
+                                    >
+                                        <Add />
+                                    </Button>
+                                </Grid>
+                            </Grid>
+                        </Grid>
+                    </DialogContent>
+
+                    <DialogActions>
+                        <Grid container justifyContent="space-between" sx={ActionContentSx}>
+                            {actionEdit && (
+                                <Grid item xs={6}>
+                                    <Button variant="outlined" color="error" onClick={onConfirmDialogOpen}>
+                                        Delete
+                                    </Button>
+                                </Grid>
+                            )}
+                            <Grid item container xs={actionEdit ? 6 : 12} justifyContent="flex-end">
+                                <Grid item sx={CancelBtnSx}>
+                                    <Button variant="outlined" color="inherit" onClick={close}>
+                                        Cancel
+                                    </Button>
+                                </Grid>
+                                <Grid item>
+                                    <Button
+                                        variant="contained"
+                                        type="submit"
+                                        disabled={!form.values.config || !form.values.name}
+                                    >
+                                        {actionEdit ? "Apply" : "Create"}
+                                    </Button>
+                                </Grid>
+                            </Grid>
+                        </Grid>
+                    </DialogActions>
+                </form>
+            </Dialog>
+
+            <Dialog onClose={onConfirmDialogClose} open={confirmDialogOpen}>
+                <DialogTitle>
+                    <Grid container direction="row" justifyContent="space-between" alignItems="center">
+                        <Typography variant="h5">Delete Scenario</Typography>
+                        <IconButton aria-label="close" onClick={onConfirmDialogClose} sx={IconButtonSx}>
+                            <Close />
+                        </IconButton>
+                    </Grid>
+                </DialogTitle>
+                <DialogContent dividers>
+                    <Typography>Are you sure you want to delete this scenario?</Typography>
+                </DialogContent>
+
+                <DialogActions>
+                    <Button variant="outlined" color="inherit" onClick={onConfirmDialogClose}>
+                        CANCEL
+                    </Button>
+                    <Button variant="contained" color="error" onClick={onDeleteScenario}>
+                        Delete
+                    </Button>
+                </DialogActions>
+            </Dialog>
+        </>
+    );
 };
 
 const ScenarioSelector = (props: ScenarioSelectorProps) => {
-  const { id = "", scenarios = [], propagate = true } = props;
-  const [open, setOpen] = useState(false);
-  const [actionEdit, setActionEdit] = useState<boolean>(false);
-
-  const dispatch = useDispatch();
-  const module = useModule();
-
-  useDispatchRequestUpdateOnFirstRender(dispatch, "", module, props.updateVars);
-
-  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 onDialogOpen = useCallback(() => {
-    setOpen(true);
-    setActionEdit(false);
-  }, []);
-
-  const onDialogClose = useCallback(() => {
-    setOpen(false);
-    setActionEdit(false);
-  }, []);
-
-  const openEditDialog = useCallback(
-    (e: React.MouseEvent<HTMLElement>) => {
-      const { id: scenId } = e.currentTarget?.dataset || {};
-      scenId && props.onScenarioSelect && dispatch(createSendActionNameAction(id, module, props.onScenarioSelect, scenId));
-      setOpen(true);
-      setActionEdit(true);
-      return false;
-    },
-    [props.onScenarioSelect]
-  );
-
-  const onSubmit = useCallback(
-    (...values: any[]) => dispatch(createSendActionNameAction(id, module, props.onScenarioCrud, ...values)),
-    [id, module, props.onScenarioCrud]
-  );
-
-  // Refresh on broadcast
-  useEffect(() => {
-    if (props.coreChanged?.scenario) {
-      const updateVar = getUpdateVar(props.updateVars, "scenarios");
-      updateVar && dispatch(createRequestUpdateAction(id, module, [updateVar], true));
-    }
-  }, [props.coreChanged, props.updateVars, module, dispatch]);
-
-  const onSelect = useCallback(
-    (e: React.SyntheticEvent, nodeIds: Array<string> | string) => {
-      const { cycle = false } = (e.currentTarget as HTMLElement)?.parentElement?.dataset || {};
-      if (cycle) {
-        return;
-      }
-      const scenariosVar = getUpdateVar(props.updateVars, "scenarios");
-      dispatch(createSendUpdateAction(props.updateVarName, nodeIds, module, props.onChange, propagate, scenariosVar));
-    },
-    [props.updateVarName, props.updateVars, props.onChange, propagate, module]
-  );
-
-  return (
-    <div>
-      <Box sx={MainBoxSx}>
-        <TreeView defaultCollapseIcon={<ExpandMore />} defaultExpandIcon={<ChevronRight />} sx={TreeViewSx} onNodeSelect={onSelect}>
-          {scenarios
-            ? scenarios.map((item) => {
-                const [id, label, scenarios, nodeType, _] = item;
-                return (
-                  <>
-                    {displayCycles ? (
-                      nodeType === NodeType.CYCLE ? (
-                        <TreeItem key={id} nodeId={id} label={label} sx={CycleSx} data-cycle>
-                          <ScenarioNodes scenarios={scenarios} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
-                        </TreeItem>
-                      ) : (
-                        <ScenarioNodes scenarios={item as Scenario} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
-                      )
-                    ) : nodeType === NodeType.SCENARIO ? (
-                      <ScenarioNodes scenarios={item as Scenario} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
-                    ) : (
-                      <ScenarioNodes scenarios={scenarios} showPrimary={showPrimaryFlag} openEditDialog={openEditDialog} />
-                    )}
-                  </>
-                );
-              })
-            : null}
-        </TreeView>
-
-        {showAddButton ? (
-          <Button variant="outlined" onClick={onDialogOpen} fullWidth>
-            ADD SCENARIO &nbsp;&nbsp;
-            <Add />
-          </Button>
-        ) : null}
-
-        <Box>{props.error}</Box>
-      </Box>
-
-      <ScenarioEditDialog
-        close={onDialogClose}
-        actionEdit={actionEdit}
-        open={open}
-        configs={props.configs}
-        scenario={props.scenarioEdit}
-        submit={onSubmit}
-      ></ScenarioEditDialog>
-    </div>
-  );
+    const { id = "", scenarios = [], propagate = true } = props;
+    const [open, setOpen] = useState(false);
+    const [actionEdit, setActionEdit] = useState<boolean>(false);
+
+    const dispatch = useDispatch();
+    const module = useModule();
+
+    useDispatchRequestUpdateOnFirstRender(dispatch, "", module, props.updateVars);
+
+    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 onDialogOpen = useCallback(() => {
+        setOpen(true);
+        setActionEdit(false);
+    }, []);
+
+    const onDialogClose = useCallback(() => {
+        setOpen(false);
+        setActionEdit(false);
+    }, []);
+
+    const openEditDialog = useCallback(
+        (e: React.MouseEvent<HTMLElement>) => {
+            const { id: scenId } = e.currentTarget?.dataset || {};
+            scenId &&
+                props.onScenarioSelect &&
+                dispatch(createSendActionNameAction(id, module, props.onScenarioSelect, scenId));
+            setOpen(true);
+            setActionEdit(true);
+            return false;
+        },
+        [props.onScenarioSelect]
+    );
+
+    const onSubmit = useCallback(
+        (...values: any[]) => dispatch(createSendActionNameAction(id, module, props.onScenarioCrud, ...values)),
+        [id, module, props.onScenarioCrud]
+    );
+
+    // Refresh on broadcast
+    useEffect(() => {
+        if (props.coreChanged?.scenario) {
+            const updateVar = getUpdateVar(props.updateVars, "scenarios");
+            updateVar && dispatch(createRequestUpdateAction(id, module, [updateVar], true));
+        }
+    }, [props.coreChanged, props.updateVars, module, dispatch]);
+
+    const onSelect = useCallback(
+        (e: React.SyntheticEvent, nodeIds: Array<string> | string) => {
+            const { cycle = false } = (e.currentTarget as HTMLElement)?.parentElement?.dataset || {};
+            if (cycle) {
+                return;
+            }
+            const scenariosVar = getUpdateVar(props.updateVars, "scenarios");
+            dispatch(
+                createSendUpdateAction(props.updateVarName, nodeIds, module, props.onChange, propagate, scenariosVar)
+            );
+        },
+        [props.updateVarName, props.updateVars, props.onChange, propagate, module]
+    );
+
+    return (
+        <div>
+            <Box sx={MainBoxSx}>
+                <TreeView
+                    defaultCollapseIcon={<ExpandMore />}
+                    defaultExpandIcon={<ChevronRight />}
+                    sx={TreeViewSx}
+                    onNodeSelect={onSelect}
+                >
+                    {scenarios
+                        ? scenarios.map((item) => {
+                              const [id, label, scenarios, nodeType, _] = item;
+                              return (
+                                  <>
+                                      {displayCycles ? (
+                                          nodeType === NodeType.CYCLE ? (
+                                              <TreeItem
+                                                  key={id}
+                                                  nodeId={id}
+                                                  label={
+                                                      <Box sx={treeItemLabelSx}>
+                                                          <Cycle fontSize="small" color="primary" />
+                                                          {label}
+                                                      </Box>
+                                                  }
+                                                  sx={CycleSx}
+                                                  data-cycle
+                                              >
+                                                  <ScenarioNodes
+                                                      scenarios={scenarios}
+                                                      showPrimary={showPrimaryFlag}
+                                                      openEditDialog={openEditDialog}
+                                                  />
+                                              </TreeItem>
+                                          ) : (
+                                              <ScenarioNodes
+                                                  scenarios={item as Scenario}
+                                                  showPrimary={showPrimaryFlag}
+                                                  openEditDialog={openEditDialog}
+                                              />
+                                          )
+                                      ) : nodeType === NodeType.SCENARIO ? (
+                                          <ScenarioNodes
+                                              scenarios={item as Scenario}
+                                              showPrimary={showPrimaryFlag}
+                                              openEditDialog={openEditDialog}
+                                          />
+                                      ) : (
+                                          <ScenarioNodes
+                                              scenarios={scenarios}
+                                              showPrimary={showPrimaryFlag}
+                                              openEditDialog={openEditDialog}
+                                          />
+                                      )}
+                                  </>
+                              );
+                          })
+                        : null}
+                </TreeView>
+
+                {showAddButton ? (
+                    <Button variant="outlined" onClick={onDialogOpen} fullWidth endIcon={<Add />}>
+                        Add scenario
+                    </Button>
+                ) : null}
+
+                <Box>{props.error}</Box>
+            </Box>
+
+            <ScenarioEditDialog
+                close={onDialogClose}
+                actionEdit={actionEdit}
+                open={open}
+                configs={props.configs}
+                scenario={props.scenarioEdit}
+                submit={onSubmit}
+            ></ScenarioEditDialog>
+        </div>
+    );
 };
 
 export default ScenarioSelector;

+ 8 - 0
gui/src/icons/cycle.component.tsx

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

+ 9 - 0
gui/src/icons/datanode.component.tsx

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

+ 6 - 0
gui/src/icons/index.ts

@@ -0,0 +1,6 @@
+export { Cycle } from "./cycle.component";
+export { Datanode } from "./datanode.component";
+export { Job } from "./job.component";
+export { Pipeline } from "./pipeline.component";
+export { Scenario } from "./scenario.component";
+export { Task } from "./task.component";

+ 9 - 0
gui/src/icons/job.component.tsx

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

+ 7 - 0
gui/src/icons/pipeline.component.tsx

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

+ 7 - 0
gui/src/icons/scenario.component.tsx

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

+ 7 - 0
gui/src/icons/task.component.tsx

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