Browse Source

#291 lock management (#305)

* #291 lock management
#264 hide GuiCoreLib
#303 refresh datanode viewer on id change

* black

* refine/fix lock management

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 1 year ago
parent
commit
1c43693700

+ 2 - 2
gui/src/DataNodeChart.tsx

@@ -369,8 +369,8 @@ const DataNodeChart = (props: DataNodeChartProps) => {
                     />
                 </Grid>
                 <Grid item>
-                    <Button onClick={resetConfig} variant="outlined" color="inherit" className="taipy-button">
-                        <RefreshOutlined /> Reset
+                    <Button onClick={resetConfig} variant="text" color="primary" className="taipy-button">
+                        <RefreshOutlined /> Reset View
                     </Button>
                 </Grid>
             </Grid>

+ 31 - 15
gui/src/DataNodeTable.tsx

@@ -11,25 +11,26 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useEffect, useState, useCallback, useMemo, MouseEvent, ChangeEvent } from "react";
+import React, { useEffect, useState, useCallback, useMemo, MouseEvent, ChangeEvent, MutableRefObject } from "react";
 
-import { TableChartOutlined, BarChartOutlined, Edit, EditOff, RefreshOutlined } from "@mui/icons-material";
+import { TableChartOutlined, BarChartOutlined, RefreshOutlined } from "@mui/icons-material";
 import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
 import Checkbox from "@mui/material/Checkbox";
 import FormControl from "@mui/material/FormControl";
+import FormControlLabel from "@mui/material/FormControlLabel";
 import Grid from "@mui/material/Grid";
-import IconButton from "@mui/material/IconButton";
 import InputLabel from "@mui/material/InputLabel";
 import OutlinedInput from "@mui/material/OutlinedInput";
 import ListItemText from "@mui/material/ListItemText";
 import MenuItem from "@mui/material/MenuItem";
 import Select, { SelectChangeEvent } from "@mui/material/Select";
+import Switch from "@mui/material/Switch";
 import TextField from "@mui/material/TextField";
 import ToggleButton from "@mui/material/ToggleButton";
 import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
 
-import { ColumnDesc, Table, TraceValueType } from "taipy-gui";
+import { ColumnDesc, Table, TraceValueType, createSendActionNameAction, useDispatch, useModule } from "taipy-gui";
 
 import { ChartViewType, MenuProps, TableViewType, selectSx, tabularHeaderSx } from "./utils";
 
@@ -43,11 +44,17 @@ interface DataNodeTableProps {
     uniqid: string;
     onEdit?: string;
     onViewTypeChange: (e: MouseEvent, value?: string) => void;
+    onLock?: string;
+    editInProgress?: boolean;
+    editLock: MutableRefObject<boolean>
 }
 
 const DataNodeTable = (props: DataNodeTableProps) => {
     const { uniqid, configId, nodeId, columns = "", onViewTypeChange } = props;
 
+    const dispatch = useDispatch();
+    const module = useModule();
+
     // tabular selected columns
     const [selectedCols, setSelectedCols] = useState<string[]>([]);
     const onColsChange = useCallback(
@@ -94,7 +101,15 @@ const DataNodeTable = (props: DataNodeTableProps) => {
     }, [columns, selectedCols]);
 
     const [tableEdit, setTableEdit] = useState(false);
-    const toggleTableEdit = useCallback(() => setTableEdit((e) => !e), []);
+    const toggleTableEdit = useCallback(
+        () =>
+            setTableEdit((e) => {
+                props.editLock.current = !e;
+                dispatch(createSendActionNameAction("", module, props.onLock, { id: nodeId, lock: !e }));
+                return !e;
+            }),
+        [nodeId, dispatch, module, props.onLock, props.editLock]
+    );
 
     const userData = useMemo(() => ({ dn_id: nodeId, comment: "" }), [nodeId]);
     const [comment, setComment] = useState("");
@@ -144,22 +159,23 @@ const DataNodeTable = (props: DataNodeTableProps) => {
                     </FormControl>
                 </Grid>
                 <Grid item>
-                    <Button onClick={resetCols} variant="outlined" color="inherit" className="taipy-button">
-                        <RefreshOutlined /> Reset
+                    <Button onClick={resetCols} variant="text" color="primary" className="taipy-button">
+                        <RefreshOutlined /> Reset View
                     </Button>
                 </Grid>
-                <Grid item>
-                    {props.active ? (
-                        <IconButton onClick={toggleTableEdit} color="primary">
-                            {tableEdit ? <EditOff /> : <Edit />}
-                        </IconButton>
-                    ) : null}
-                </Grid>
                 {tableEdit ? (
-                    <Grid item>
+                    <Grid item sx={{ ml: "auto" }}>
                         <TextField value={comment} onChange={changeComment} label="Comment"></TextField>
                     </Grid>
                 ) : null}
+                <Grid item sx={tableEdit ? undefined : { ml: "auto" }}>
+                    <FormControlLabel
+                        disabled={!props.active || !!props.editInProgress}
+                        control={<Switch color="primary" checked={tableEdit} onChange={toggleTableEdit} />}
+                        label="Edit data"
+                        labelPlacement="start"
+                    />
+                </Grid>
             </Grid>
             <Table
                 active={props.active}

+ 77 - 10
gui/src/DataNodeViewer.tsx

@@ -11,7 +11,18 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useState, useCallback, useEffect, useMemo, ChangeEvent, SyntheticEvent, MouseEvent } from "react";
+import React, {
+    useState,
+    useCallback,
+    useContext,
+    useEffect,
+    useMemo,
+    ChangeEvent,
+    SyntheticEvent,
+    MouseEvent,
+    useRef,
+} from "react";
+import { CheckCircle, Cancel, ArrowForwardIosSharp, Launch, LockOutlined } from "@mui/icons-material";
 import Accordion from "@mui/material/Accordion";
 import AccordionDetails from "@mui/material/AccordionDetails";
 import AccordionSummary from "@mui/material/AccordionSummary";
@@ -25,8 +36,8 @@ 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 Tooltip from "@mui/material/Tooltip";
 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";
@@ -35,14 +46,15 @@ import { format } from "date-fns";
 
 import {
     ColumnDesc,
+    Context,
     RowValue,
     TableValueType,
     createRequestUpdateAction,
     createSendActionNameAction,
     getUpdateVar,
-    useDispatch,
     useDynamicProperty,
     useModule,
+    Store,
 } from "taipy-gui";
 
 import { Cycle as CycleIcon, Scenario as ScenarioIcon } from "./icons";
@@ -79,7 +91,20 @@ const editSx = {
 };
 const textFieldProps = { textField: { margin: "dense" } } as BaseDateTimePickerSlotsComponentsProps<Date>;
 
-type DataNodeFull = [string, string, string, string, string, string, string, string, number, Array<[string, string]>];
+type DataNodeFull = [
+    string,
+    string,
+    string,
+    string,
+    string,
+    string,
+    string,
+    string,
+    number,
+    Array<[string, string]>,
+    boolean,
+    string
+];
 
 enum DataNodeFullProps {
     id,
@@ -92,6 +117,8 @@ enum DataNodeFullProps {
     ownerLabel,
     ownerType,
     properties,
+    editInProgress,
+    editorId,
 }
 const DataNodeFullLength = Object.keys(DataNodeFullProps).length / 2;
 
@@ -137,6 +164,7 @@ interface DataNodeViewerProps {
     onTabularDataEdit?: string;
     chartConfig?: string;
     width?: string;
+    onLock?: string;
 }
 
 const getDataValue = (value?: RowValue, dType?: string | null) => (dType == "date" ? new Date(value as string) : value);
@@ -162,9 +190,12 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         showData = true,
     } = props;
 
-    const dispatch = useDispatch();
+    const { state, dispatch } = useContext<Store>(Context);
     const module = useModule();
     const uniqid = useUniqueId(id);
+    const editorId = (state as { id: string }).id;
+    const editLock = useRef(false);
+    const oldId = useRef<string|undefined>(undefined);
 
     const [
         dnId,
@@ -177,6 +208,8 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         dnOwnerLabel,
         dnOwnerType,
         dnProperties,
+        dnEditInProgress,
+        dnEditorId,
         isDataNode,
     ] = useMemo(() => {
         let dn: DataNodeFull | undefined = undefined;
@@ -189,8 +222,19 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                 // DO nothing
             }
         }
-        return dn ? [...dn, true] : ["", "", "", "", "", "", "", "", -1, [], false];
-    }, [props.dataNode, props.defaultDataNode]);
+        // clean lock on change
+        if (dn && dn[DataNodeFullProps.id] !== oldId.current) {
+            oldId.current && editLock.current && dispatch(createSendActionNameAction(id, module, props.onLock, { id: oldId.current, lock: false }));
+            editLock.current = false;
+            oldId.current = dn[DataNodeFullProps.id];
+        }
+        return dn ? [...dn, true] : ["", "", "", "", "", "", "", "", -1, [], false, "", false];
+    }, [props.dataNode, props.defaultDataNode, oldId, id, dispatch, module, props.onLock]);
+
+    // clean lock on unmount
+    useEffect(() => () => {
+        oldId.current && editLock.current && dispatch(createSendActionNameAction(id, module, props.onLock, { id: oldId.current, lock: false }));
+    }, [id, dispatch, module, props.onLock]);
 
     const active = useDynamicProperty(props.active, props.defaultActive, true);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
@@ -267,7 +311,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         setDataRequested(false);
         setViewType(TableViewType);
         setComment("");
-    }, [dnLabel, isDataNode, expanded]);
+    }, [dnId, dnLabel, isDataNode, expanded]);
 
     // Datanode data
     const dtValue = (props.data && props.data[DatanodeDataProps.value]) ?? undefined;
@@ -388,6 +432,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                             <Grid item>
                                 <Typography fontSize="smaller">{dnType}</Typography>
                             </Grid>
+                            <Grid item>{}</Grid>
                         </Grid>
                     </AccordionSummary>
                     <AccordionDetails>
@@ -405,7 +450,23 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                     style={showHistory ? undefined : noDisplay}
                                 />
                                 <Tab
-                                    label="Data"
+                                    label={
+                                        <Grid container alignItems="center">
+                                            <Grid item>Data</Grid>
+                                            {dnEditInProgress ? (
+                                                <Grid item>
+                                                    <Tooltip
+                                                        title={"locked " + (dnEditorId === editorId ? "by you" : "")}
+                                                    >
+                                                        <LockOutlined
+                                                            fontSize="small"
+                                                            color={dnEditorId === editorId ? "disabled" : "primary"}
+                                                        />
+                                                    </Tooltip>
+                                                </Grid>
+                                            ) : null}
+                                        </Grid>
+                                    }
                                     id={`${uniqid}-data`}
                                     aria-controls={`${uniqid}-dn-tabpanel-data`}
                                     style={showData ? undefined : noDisplay}
@@ -615,7 +676,10 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                             onClick={onFocus}
                                             sx={hoverSx}
                                         >
-                                            {active && focusName === "data-value" ? (
+                                            {active &&
+                                            dnEditInProgress &&
+                                            dnEditorId === editorId &&
+                                            focusName === "data-value" ? (
                                                 <>
                                                     {typeof dtValue == "boolean" ? (
                                                         <>
@@ -744,6 +808,9 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                                 onViewTypeChange={onViewTypeChange}
                                                 updateVarName={getUpdateVar(props.updateVars, "tabularData")}
                                                 onEdit={props.onTabularDataEdit}
+                                                onLock={props.onLock}
+                                                editInProgress={dnEditInProgress && dnEditorId !== editorId}
+                                                editLock={editLock}
                                             />
                                         ) : (
                                             <DataNodeChart

+ 1 - 0
src/taipy/gui_core/GuiCoreLib.py → src/taipy/gui_core/_GuiCoreLib.py

@@ -179,6 +179,7 @@ class _GuiCore(ElementLibrary):
                 "on_tabular_data_edit": ElementProperty(
                     PropertyType.function, f"{{{__CTX_VAR_NAME}.tabular_data_edit}}"
                 ),
+                "on_lock": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.lock_datanode_for_edit}}"),
             },
         ),
         "job_selector": Element(

+ 3 - 1
src/taipy/gui_core/_adapters.py

@@ -60,7 +60,7 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                     if scenario.sequences
                     else [],
                     list(scenario.properties.get("authorized_tags", [])) if scenario.properties else [],
-                    is_deletable(scenario),  # deletable
+                    is_deletable(scenario),
                     is_promotable(scenario),
                     is_submittable(scenario),
                 ]
@@ -141,6 +141,8 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
                         for k, v in datanode._get_user_properties().items()
                         if k not in _GuiCoreDatanodeAdapter.__INNER_PROPS
                     ],
+                    datanode._edit_in_progress,
+                    datanode._editor_id,
                 ]
         return None
 

+ 21 - 3
src/taipy/gui_core/_context.py

@@ -260,7 +260,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             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}")
+                            _warn(f"on_creation(): Exception raised in '{on_creation}()'", e)
                 else:
                     _warn(f"on_creation(): '{on_creation}' is not a function.")
                 scenario = create_scenario(scenario_config, date, name)
@@ -424,7 +424,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     self.client_jobs_by_submission[sub_id] = sub_details.set_status(new_status)
 
         except Exception as e:
-            _warn(f"Job is not available {e}")
+            _warn("Job is not available", e)
 
     def __do_datanodes_tree(self):
         if self.data_nodes_by_owner is None:
@@ -524,6 +524,24 @@ class _GuiCoreContext(CoreEventConsumerBase):
             except Exception as e:
                 state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode. {e}")
 
+    def lock_datanode_for_edit(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)
+        lock = data.get("lock", True)
+        entity: DataNode = core_get(entity_id)
+        if isinstance(entity, DataNode):
+            try:
+                if lock:
+                    entity.lock_edit(self.gui._get_client_id())
+                else:
+                    entity.unlock_edit(self.gui._get_client_id())
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+            except Exception as e:
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error locking Datanode. {e}")
+
     def __edit_properties(self, entity: t.Union[Scenario, Sequence, DataNode], data: t.Dict[str, str]):
         with entity as ent:
             if isinstance(ent, Scenario):
@@ -597,7 +615,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             and (dn := core_get(id))
             and isinstance(dn, DataNode)
         ):
-            if dn.is_ready_for_reading:
+            if dn._last_edit_date:
                 if isinstance(dn, _AbstractTabularDataNode):
                     return (None, None, True, None)
                 try:

+ 1 - 1
src/taipy/gui_core/_init.py

@@ -9,7 +9,7 @@
 # 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 .GuiCoreLib import _GuiCore
+from ._GuiCoreLib import _GuiCore
 
 
 def _init_gui_core():