Explorar el Código

Property to not show the creation dialog (#725)

* Property to not show a creation dialog
scenario to limit datanode_selector list

* mypy and fix tests

* mypy

* fix tests

* Fab's comment

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide hace 1 año
padre
commit
413be6ec14

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 328 - 220
frontend/taipy/package-lock.json


+ 31 - 26
frontend/taipy/src/ScenarioSelector.tsx

@@ -84,6 +84,7 @@ interface ScenarioSelectorProps {
     className?: string;
     dynamicClassName?: string;
     showPins?: boolean;
+    showDialog?: boolean;
 }
 
 interface ScenarioEditDialogProps {
@@ -277,7 +278,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                                         <DatePicker
                                             label="Date"
                                             value={new Date(form.values.date)}
-                                            onChange={(date) => form.setFieldValue("date", date?.toISOString())}
+                                            onChange={(date?:Date|null) => form.setFieldValue("date", date?.toISOString())}
                                             disabled={actionEdit}
                                         />
                                     </LocalizationProvider>
@@ -410,7 +411,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
 };
 
 const ScenarioSelector = (props: ScenarioSelectorProps) => {
-    const { showAddButton = true, propagate = true, showPins = false } = props;
+    const { showAddButton = true, propagate = true, showPins = false, showDialog = true } = props;
     const [open, setOpen] = useState(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
 
@@ -419,32 +420,9 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
     const dispatch = useDispatch();
     const module = useModule();
 
-    const onDialogOpen = useCallback(() => {
-        setOpen(true);
-        setActionEdit(false);
-    }, []);
-
-    const onDialogClose = useCallback(() => {
-        setOpen(false);
-        setActionEdit(false);
-    }, []);
-
-    const openEditDialog = useCallback(
-        (e: React.MouseEvent<HTMLElement>) => {
-            e.stopPropagation();
-            const { id: scenId } = e.currentTarget?.dataset || {};
-            scenId &&
-                props.onScenarioSelect &&
-                dispatch(createSendActionNameAction(props.id, module, props.onScenarioSelect, scenId));
-            setOpen(true);
-            setActionEdit(true);
-        },
-        [props.onScenarioSelect, props.id, dispatch, module]
-    );
-
     const onSubmit = useCallback(
         (...values: unknown[]) => {
-            dispatch(createSendActionNameAction(props.id, module, props.onScenarioCrud, ...values, props.onCreation));
+            dispatch(createSendActionNameAction(props.id, module, props.onScenarioCrud, props.onCreation, ...values));
             if (values.length > 1 && values[1]) {
                 // delete requested => unselect current node
                 const lovVar = getUpdateVar(props.updateVars, "scenarios");
@@ -466,6 +444,33 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
         ]
     );
 
+    const onDialogOpen = useCallback(() => {
+        if (showDialog) {
+            setOpen(true);
+            setActionEdit(false);
+        } else {
+            onSubmit(false, false, {}, false);
+        }
+    }, [showDialog, onSubmit]);
+
+    const onDialogClose = useCallback(() => {
+        setOpen(false);
+        setActionEdit(false);
+    }, []);
+
+    const openEditDialog = useCallback(
+        (e: React.MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            const { id: scenId } = e.currentTarget?.dataset || {};
+            scenId &&
+                props.onScenarioSelect &&
+                dispatch(createSendActionNameAction(props.id, module, props.onScenarioSelect, scenId));
+            setOpen(true);
+            setActionEdit(true);
+        },
+        [props.onScenarioSelect, props.id, dispatch, module]
+    );
+
     const EditScenario = useCallback(
         (props: EditProps) => (
             <Tooltip title="Edit Scenario">

+ 12 - 2
taipy/gui_core/_GuiCoreLib.py

@@ -16,7 +16,12 @@ from taipy.gui import Gui, State
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
 
 from ..version import _get_version
-from ._adapters import _GuiCoreDatanodeAdapter, _GuiCoreScenarioAdapter, _GuiCoreScenarioDagAdapter
+from ._adapters import (
+    _GuiCoreDatanodeAdapter,
+    _GuiCoreScenarioAdapter,
+    _GuiCoreScenarioDagAdapter,
+    _GuiCoreScenarioNoUpdate,
+)
 from ._context import _GuiCoreContext
 
 
@@ -41,6 +46,7 @@ class _GuiCore(ElementLibrary):
                 "class_name": ElementProperty(PropertyType.dynamic_string),
                 "show_pins": ElementProperty(PropertyType.boolean, False),
                 "on_creation": ElementProperty(PropertyType.function),
+                "show_dialog": ElementProperty(PropertyType.boolean, True),
             },
             inner_properties={
                 "scenarios": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_scenarios()}}"),
@@ -109,9 +115,13 @@ class _GuiCore(ElementLibrary):
                 "height": ElementProperty(PropertyType.string, "50vh"),
                 "class_name": ElementProperty(PropertyType.dynamic_string),
                 "show_pins": ElementProperty(PropertyType.boolean, True),
+                _GuiCoreContext._DATANODE_SEL_SCENARIO_PROP: ElementProperty(_GuiCoreScenarioNoUpdate),
             },
             inner_properties={
-                "datanodes": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_datanodes_tree()}}"),
+                "datanodes": ElementProperty(
+                    PropertyType.lov,
+                    f"{{{__CTX_VAR_NAME}.get_datanodes_tree(<tp:prop:{_GuiCoreContext._DATANODE_SEL_SCENARIO_PROP}>)}}",
+                ),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "type": ElementProperty(PropertyType.inner, __DATANODE_ADAPTER),
             },

+ 6 - 0
taipy/gui_core/_adapters.py

@@ -145,6 +145,12 @@ class _GuiCoreScenarioDagAdapter(_TaipyBase):
         return _TaipyBase._HOLDER_PREFIX + "ScG"
 
 
+class _GuiCoreScenarioNoUpdate(_TaipyBase, _DoNotUpdate):
+    @staticmethod
+    def get_hash():
+        return _TaipyBase._HOLDER_PREFIX + "ScN"
+
+
 class _GuiCoreDatanodeAdapter(_TaipyBase):
     __INNER_PROPS = ["name"]
 

+ 81 - 78
taipy/gui_core/_context.py

@@ -33,6 +33,8 @@ from taipy.core import (
     ScenarioId,
     Sequence,
     SequenceId,
+    Submission,
+    SubmissionId,
     cancel_job,
     create_scenario,
     delete_job,
@@ -53,8 +55,6 @@ from taipy.core.data._abstract_tabular import _AbstractTabularDataNode
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
 from taipy.core.notification.event import Event, EventOperation
 from taipy.core.notification.notifier import Notifier
-from taipy.core.submission._submission_manager_factory import _SubmissionManagerFactory
-from taipy.core.submission.submission import Submission
 from taipy.core.submission.submission_status import SubmissionStatus
 from taipy.gui import Gui, State
 from taipy.gui._warnings import _warn
@@ -63,24 +63,6 @@ from taipy.gui.gui import _DoNotUpdate
 from ._adapters import _EntityType
 
 
-class _SubmissionDetails:
-    def __init__(
-        self,
-        client_id: str,
-        module_context: str,
-        callback: t.Callable,
-        submission_status: SubmissionStatus,
-    ) -> None:
-        self.client_id = client_id
-        self.module_context = module_context
-        self.callback = callback
-        self.submission_status = submission_status
-
-    def set_submission_status(self, submission_status: SubmissionStatus):
-        self.submission_status = submission_status
-        return self
-
-
 class _GuiCoreContext(CoreEventConsumerBase):
     __PROP_ENTITY_ID = "id"
     __PROP_ENTITY_COMMENT = "comment"
@@ -102,6 +84,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
     _DATANODE_VIZ_DATA_ID_VAR = "gui_core_dv_data_id"
     _DATANODE_VIZ_DATA_CHART_ID_VAR = "gui_core_dv_data_chart_id"
     _DATANODE_VIZ_DATA_NODE_PROP = "data_node"
+    _DATANODE_SEL_SCENARIO_PROP = "scenario"
 
     def __init__(self, gui: Gui) -> None:
         self.gui = gui
@@ -109,7 +92,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         self.data_nodes_by_owner: t.Optional[t.Dict[t.Optional[str], DataNode]] = None
         self.scenario_configs: t.Optional[t.List[t.Tuple[str, str]]] = None
         self.jobs_list: t.Optional[t.List[Job]] = None
-        self.client_submission: t.Dict[str, _SubmissionDetails] = dict()
+        self.client_submission: t.Dict[str, SubmissionStatus] = dict()
         # register to taipy core notification
         reg_id, reg_queue = Notifier.register()
         # locks
@@ -121,9 +104,6 @@ class _GuiCoreContext(CoreEventConsumerBase):
 
     def process_event(self, event: Event):
         if event.entity_type == EventEntityType.SCENARIO:
-            if event.operation == EventOperation.SUBMISSION:
-                self.scenario_status_callback(event.attribute_name)
-                return
             self.scenario_refresh(
                 event.entity_id
                 if event.operation != EventOperation.DELETION and is_readable(t.cast(ScenarioId, event.entity_id))
@@ -147,6 +127,15 @@ class _GuiCoreContext(CoreEventConsumerBase):
         elif event.entity_type == EventEntityType.JOB:
             with self.lock:
                 self.jobs_list = None
+            if event.operation == EventOperation.UPDATE:
+                try:
+                    job_entity = t.cast(Job, core_get(str(event.entity_id)))
+                    self.gui._broadcast(
+                        _GuiCoreContext._CORE_CHANGED_NAME,
+                        {"task": {"id": job_entity.task.id, "status": job_entity.status.name}},
+                    )
+                except Exception as e:
+                    _warn(f"Access to sequence {event.entity_id} failed", e)
         elif event.entity_type == EventEntityType.SUBMISSION:
             self.scenario_status_callback(event.entity_id)
         elif event.entity_type == EventEntityType.DATA_NODE:
@@ -167,14 +156,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
         )
 
     def scenario_status_callback(self, submission_id: t.Optional[str]):
-        if not submission_id or not is_readable_submission(submission_id):
+        if not submission_id or not is_readable(t.cast(SubmissionId, submission_id)):
             return
         try:
-            sub_details: t.Optional[_SubmissionDetails] = self.client_submission.get(submission_id)
-            if not sub_details:
+            last_status = self.client_submission.get(submission_id)
+            if not last_status:
                 return
 
-            submission = core_get_submission(submission_id)
+            submission = t.cast(Submission, core_get(submission_id))
             if not submission or not submission.entity_id:
                 return
 
@@ -183,13 +172,19 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 return
 
             new_status = submission.submission_status
-            if sub_details.submission_status != new_status:
+            if last_status != new_status:
                 # callback
+                submission_name = submission.properties.get("on_submission")
+                if not submission_name:
+                    return
+                submission_fn = self.gui._get_user_function(submission_name)
+                if not callable(submission_fn):
+                    return
                 self.gui._call_user_callback(
-                    sub_details.client_id,
-                    sub_details.callback,
+                    submission.properties.get("client_id"),
+                    submission_fn,
                     [entity, {"submission_status": new_status.name}],
-                    sub_details.module_context,
+                    submission.properties.get("module_context"),
                 )
             with self.submissions_lock:
                 if new_status in (
@@ -199,7 +194,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 ):
                     self.client_submission.pop(submission_id, None)
                 else:
-                    self.client_submission[submission_id] = sub_details.set_submission_status(new_status)
+                    self.client_submission[submission_id] = new_status
 
         except Exception as e:
             _warn(f"Submission ({submission_id}) is not available", e)
@@ -278,15 +273,16 @@ class _GuiCoreContext(CoreEventConsumerBase):
         if (
             args is None
             or not isinstance(args, list)
-            or len(args) < 3
-            or not isinstance(args[0], bool)
+            or len(args) < 4
             or not isinstance(args[1], bool)
-            or not isinstance(args[2], dict)
+            or not isinstance(args[2], bool)
+            or not isinstance(args[3], dict)
         ):
             return
-        update = args[0]
-        delete = args[1]
-        data = args[2]
+        update = args[1]
+        delete = args[2]
+        data = args[3]
+        with_dialog = True if len(args) < 5 else bool(args[4])
         scenario = None
 
         name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
@@ -309,21 +305,27 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     return
                 scenario = core_get(scenario_id)
         else:
-            config_id = data.get(_GuiCoreContext.__PROP_CONFIG_ID)
-            scenario_config = Config.scenarios.get(config_id)
-            if scenario_config is None:
-                state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid configuration id ({config_id})")
-                return
-            date_str = data.get(_GuiCoreContext.__PROP_DATE)
-            try:
-                date = parser.parse(date_str) if isinstance(date_str, str) else None
-            except Exception as e:
-                state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid date ({date_str}).{e}")
-                return
+            if with_dialog:
+                config_id = data.get(_GuiCoreContext.__PROP_CONFIG_ID)
+                scenario_config = Config.scenarios.get(config_id)
+                if with_dialog and scenario_config is None:
+                    state.assign(
+                        _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid configuration id ({config_id})"
+                    )
+                    return
+                date_str = data.get(_GuiCoreContext.__PROP_DATE)
+                try:
+                    date = parser.parse(date_str) if isinstance(date_str, str) else None
+                except Exception as e:
+                    state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid date ({date_str}).{e}")
+                    return
+            else:
+                scenario_config = None
+                date = None
             scenario_id = None
             try:
                 gui: Gui = state._gui
-                on_creation = args[3] if len(args) > 3 and isinstance(args[3], str) else None
+                on_creation = args[0] if isinstance(args[0], str) else None
                 on_creation_function = gui._get_user_function(on_creation) if on_creation else None
                 if callable(on_creation_function):
                     try:
@@ -361,6 +363,17 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         return
                 elif on_creation is not None:
                     _warn(f"on_creation(): '{on_creation}' is not a function.")
+                elif not with_dialog:
+                    if len(Config.scenarios) == 2:
+                        scenario_config = [sc for k, sc in Config.scenarios.items() if k != "default"][0]
+                    else:
+                        state.assign(
+                            _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
+                            "Error creating Scenario: only one scenario config needed "
+                            + f"({len(Config.scenarios) - 1}) found.",
+                        )
+                        return
+
                 scenario = create_scenario(scenario_config, date, name)
                 scenario_id = scenario.id
             except Exception as e:
@@ -431,22 +444,19 @@ class _GuiCoreContext(CoreEventConsumerBase):
         entity = core_get(entity_id)
         if entity:
             try:
-                submission_entity = core_submit(entity)
-                if submission_cb := data.get("on_submission_change"):
-                    submission_fn = self.gui._get_user_function(submission_cb)
-                    if callable(submission_fn):
-                        client_id = self.gui._get_client_id()
-                        module_context = self.gui._get_locals_context()
-                        with self.submissions_lock:
-                            self.client_submission[submission_entity.id] = _SubmissionDetails(
-                                client_id,
-                                module_context,
-                                submission_fn,
-                                submission_entity.submission_status,
-                            )
-                    else:
-                        _warn(f"on_submission_change(): '{submission_cb}' is not a valid function.")
-                self.scenario_status_callback(submission_entity.id)
+                on_submission = data.get("on_submission_change")
+                submission_entity = core_submit(
+                    entity,
+                    on_submission=on_submission,
+                    client_id=self.gui._get_client_id(),
+                    module_context=self.gui._get_locals_context(),
+                )
+                if on_submission:
+                    with self.submissions_lock:
+                        self.client_submission[submission_entity.id] = submission_entity.submission_status
+                    if Config.core.mode == "development":
+                        self.client_submission[submission_entity.id] = SubmissionStatus.SUBMITTED
+                        self.scenario_status_callback(submission_entity.id)
                 state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
             except Exception as e:
                 state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
@@ -457,10 +467,12 @@ class _GuiCoreContext(CoreEventConsumerBase):
             for dn in get_data_nodes():
                 self.data_nodes_by_owner[dn.owner_id].append(dn)
 
-    def get_datanodes_tree(self):
+    def get_datanodes_tree(self, scenario: t.Optional[Scenario]):
         with self.lock:
             self.__do_datanodes_tree()
-        return self.data_nodes_by_owner.get(None, []) + self.get_scenarios()
+        return (
+            self.data_nodes_by_owner.get(scenario.id if scenario else None, []) if self.data_nodes_by_owner else []
+        ) + (self.get_scenarios() if not scenario else [])
 
     def data_node_adapter(self, data):
         try:
@@ -839,12 +851,3 @@ class _GuiCoreContext(CoreEventConsumerBase):
             state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, data_id)
         elif chart_id := data.get("chart_id"):
             state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_CHART_ID_VAR, chart_id)
-
-
-# TODO remove when Submission is supported by Core API
-def is_readable_submission(id: str):
-    return _SubmissionManagerFactory._build_manager()._is_readable(t.cast(Submission, id))
-
-
-def core_get_submission(id: str):
-    return _SubmissionManagerFactory._build_manager()._get(id)

+ 11 - 0
taipy/gui_core/viselements.json

@@ -67,6 +67,12 @@
                       "type": "Callback",
                       "doc": "The name of the function that is triggered when a scenario is about to be created.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the scenario selector.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>config: the name of the selected scenario configuration.</li>\n<li>date: the creation date for the new scenario.</li>\n<li>label: the user-specified label.</li>\n<li>properties: a dictionary containing all the user-defined custom properties.</li>\n</ul>\n</li>\n<li>The callback function can return a scenario, a string containing an error message (a scenario will not be created), or None (then a new scenario is created with the user parameters).</li>\n</ul>",
                       "signature": [["state", "State"], ["id", "str"], ["payload", "dict"]]
+                    },
+                    {
+                        "name": "show_dialog",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If True, a dialog is shown when the user click on the 'Add scenario' button."
                     }
                 ]
             }
@@ -260,6 +266,11 @@
                         "type": "bool",
                         "default_value": "True",
                         "doc": "If True, a pin is shown on each item of the selector and allows to restrict the number of displayed items."
+                    },
+                    {
+                        "name": "scenario",
+                        "type": "dynamic(Scenario)",
+                        "doc": "If the <code>Scenario^</code> is set, the selector will only show datanodes owned by this scenario."
                     }
                 ]
             }

+ 2 - 0
tests/gui_core/test_context_is_deletable.py

@@ -58,6 +58,7 @@ class TestGuiCoreContext_is_deletable:
                 "",
                 {
                     "args": [
+                        "",
                         True,
                         True,
                         {"name": "name", "id": a_scenario.id},
@@ -75,6 +76,7 @@ class TestGuiCoreContext_is_deletable:
                     "",
                     {
                         "args": [
+                            "",
                             True,
                             True,
                             {"name": "name", "id": a_scenario.id},

+ 2 - 0
tests/gui_core/test_context_is_editable.py

@@ -57,6 +57,7 @@ class TestGuiCoreContext_is_editable:
                 "",
                 {
                     "args": [
+                        "",
                         True,
                         False,
                         {"name": "name", "id": a_scenario.id},
@@ -72,6 +73,7 @@ class TestGuiCoreContext_is_editable:
                     "",
                     {
                         "args": [
+                            "",
                             True,
                             False,
                             {"name": "name", "id": a_scenario.id},

+ 6 - 8
tests/gui_core/test_context_is_readable.py

@@ -17,7 +17,7 @@ from taipy.core import Job, JobId, Scenario, Task
 from taipy.core.data.pickle import PickleDataNode
 from taipy.core.submission.submission import Submission
 from taipy.gui import Gui
-from taipy.gui_core._context import _GuiCoreContext, _SubmissionDetails
+from taipy.gui_core._context import _GuiCoreContext
 
 a_scenario = Scenario("scenario_config_id", None, {}, sequences={"sequence": {}})
 a_task = Task("task_config_id", {}, print)
@@ -83,6 +83,7 @@ class TestGuiCoreContext_is_readable:
                 "",
                 {
                     "args": [
+                        "",
                         True,
                         False,
                         {"name": "name", "id": a_scenario.id},
@@ -98,6 +99,7 @@ class TestGuiCoreContext_is_readable:
                     "",
                     {
                         "args": [
+                            "",
                             True,
                             False,
                             {"name": "name", "id": a_scenario.id},
@@ -141,18 +143,14 @@ class TestGuiCoreContext_is_readable:
                 assert str(assign.call_args.args[1]).endswith("is not readable.")
 
     def test_scenario_status_callback(self):
-        with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget, patch(
-            "taipy.gui_core._context.core_get_submission", side_effect=mock_core_get
-        ):
+        with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get) as mockget:
             mockget.reset_mock()
             gui_core_context = _GuiCoreContext(Mock())
 
             def sub_cb():
                 return True
 
-            gui_core_context.client_submission[a_submission.id] = _SubmissionDetails(
-                "client_id", "", sub_cb, a_submission
-            )
+            gui_core_context.client_submission[a_submission.id] = a_submission.submission_status
             gui_core_context.scenario_status_callback(a_submission.id)
             mockget.assert_called()
             found = False
@@ -163,7 +161,7 @@ class TestGuiCoreContext_is_readable:
             assert found is True
             mockget.reset_mock()
 
-            with patch("taipy.gui_core._context.is_readable_submission", side_effect=mock_is_readable_false):
+            with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):
                 gui_core_context.scenario_status_callback(a_submission.id)
                 mockget.assert_not_called()
 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio