Browse Source

Submission status update (#1434)

* Submission status update
resolves #1419

* catch date format exception

* add tests

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 11 months ago
parent
commit
5a801cdf6c

+ 59 - 20
frontend/taipy-gui/src/utils/index.spec.ts

@@ -14,7 +14,7 @@
 import "@testing-library/jest-dom";
 import { FormatConfig } from "../context/taipyReducers";
 
-import { getNumberString } from "./index";
+import { getNumberString, getDateTimeString } from "./index";
 
 let myWarn: jest.Mock;
 
@@ -23,49 +23,88 @@ beforeEach(() => {
     console.warn = myWarn;
 })
 
-const getFormatConfig = (numberFormat?: string): FormatConfig => ({timeZone: "", date: "", dateTime: "", number: numberFormat || "", forceTZ: false})
+const getNumberFormatConfig = (numberFormat?: string): FormatConfig => ({timeZone: "", date: "", dateTime: "", number: numberFormat || "", forceTZ: false})
+const getDateFormatConfig = (dateFormat?: string): FormatConfig => ({timeZone: "", date: dateFormat || "", dateTime: dateFormat || "", number: "", forceTZ: false})
 
 describe("getNumberString", () => {
     it("returns straight", async () => {
-        expect(getNumberString(1, undefined, getFormatConfig())).toBe("1");
+        expect(getNumberString(1, undefined, getNumberFormatConfig())).toBe("1");
     });
     it("returns formatted", async () => {
-        expect(getNumberString(1, "%.1f", getFormatConfig())).toBe("1.0");
+        expect(getNumberString(1, "%.1f", getNumberFormatConfig())).toBe("1.0");
     });
     it("returns formatted float", async () => {
-        expect(getNumberString(1.0, "%.0f", getFormatConfig())).toBe("1");
+        expect(getNumberString(1.0, "%.0f", getNumberFormatConfig())).toBe("1");
     });
     it("returns default formatted", async () => {
-        expect(getNumberString(1, "", getFormatConfig("%.1f"))).toBe("1.0");
+        expect(getNumberString(1, "", getNumberFormatConfig("%.1f"))).toBe("1.0");
     });
     it("returns for non variable format", async () => {
-        expect(getNumberString(1, "toto", getFormatConfig())).toBe("toto");
+        expect(getNumberString(1, "toto", getNumberFormatConfig())).toBe("toto");
     });
     it("returns formatted over default", async () => {
-        expect(getNumberString(1, "%.2f", getFormatConfig("%.1f"))).toBe("1.00");
+        expect(getNumberString(1, "%.2f", getNumberFormatConfig("%.1f"))).toBe("1.00");
     });
     it("returns for string", async () => {
-        expect(getNumberString("null" as unknown as number, "", getFormatConfig("%.1f"))).toBe("null");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] expecting number but found string")
+        expect(getNumberString("null" as unknown as number, "", getNumberFormatConfig("%.1f"))).toBe("null");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] expecting number but found string")
     });
     it("returns for object", async () => {
-        expect(getNumberString({t: 1} as unknown as number, "", getFormatConfig("%.1f"))).toBe("");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] expecting number but found object")
+        expect(getNumberString({t: 1} as unknown as number, "", getNumberFormatConfig("%.1f"))).toBe("");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] expecting number but found object")
     });
     it("returns for bad format", async () => {
-        expect(getNumberString(1, "%.f", getFormatConfig())).toBe("1");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(1, "%.f", getNumberFormatConfig())).toBe("1");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
     });
     it("returns for null", async () => {
-        expect(getNumberString(null as unknown as number, "%2.f", getFormatConfig("%.1f"))).toBe("");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(null as unknown as number, "%2.f", getNumberFormatConfig("%.1f"))).toBe("");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
     });
     it("returns for undefined", async () => {
-        expect(getNumberString(undefined as unknown as number, "%2.f", getFormatConfig("%.1f"))).toBe("");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(undefined as unknown as number, "%2.f", getNumberFormatConfig("%.1f"))).toBe("");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
     });
     it("returns for NaN", async () => {
-        expect(getNumberString(NaN, "%2.f", getFormatConfig("%.1f"))).toBe("NaN");
-        expect(myWarn).toHaveBeenCalledWith("getNumberString: [sprintf] unexpected placeholder")
+        expect(getNumberString(NaN, "%2.f", getNumberFormatConfig("%.1f"))).toBe("NaN");
+        expect(myWarn).toHaveBeenCalledWith("Invalid number format:", "[sprintf] unexpected placeholder")
+    });
+});
+
+describe("getDateTimeString", () => {
+    it("returns straight", async () => {
+        expect(getDateTimeString("2024-10-05", undefined, getDateFormatConfig())).toContain("05 2024");
+    });
+    it("returns formatted", async () => {
+        expect(getDateTimeString("2024-10-05", "dd-MM-yy", getDateFormatConfig())).toBe("05-10-24");
+    });
+    it("returns default formatted", async () => {
+        expect(getDateTimeString("2024-10-05", "", getDateFormatConfig("dd-MM-yy"))).toBe("05-10-24");
+    });
+    it("returns formatted over default", async () => {
+        expect(getDateTimeString("2024-10-05", "dd-MM-yy", getNumberFormatConfig("yy-MM-dd"))).toBe("05-10-24");
+    });
+    it("returns for string", async () => {
+        expect(getDateTimeString("null" as unknown as string, "", getDateFormatConfig("dd-MM-yy"))).toBe("Invalid Date");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
+    });
+    it("returns for object", async () => {
+        expect(getDateTimeString({t: 1} as unknown as string, "", getDateFormatConfig("dd-MM-yy"))).toBe("Invalid Date");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
+    });
+    it("returns for bad format", async () => {
+        expect(getDateTimeString("2024-10-05", "D", getDateFormatConfig())).toContain("05 2024");
+        expect(myWarn).toHaveBeenCalled()
+        expect(myWarn.mock.lastCall).toHaveLength(2)
+        expect(myWarn.mock.lastCall[0]).toBe("Invalid date format:")
+        expect(myWarn.mock.lastCall[1]).toContain("Use `d` instead of `D`")
+    });
+    it("returns for null", async () => {
+        expect(getDateTimeString(null as unknown as string, "dd-MM-yy", getDateFormatConfig())).toBe("null");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
+    });
+    it("returns for undefined", async () => {
+        expect(getDateTimeString(undefined as unknown as string, "dd-MM-yy", getDateFormatConfig())).toBe("null");
+        expect(myWarn).toHaveBeenCalledWith("Invalid date format:", "Invalid time value")
     });
 });

+ 14 - 8
frontend/taipy-gui/src/utils/index.ts

@@ -105,14 +105,20 @@ export const getDateTimeString = (
     tz?: string,
     withTime: boolean = true
 ): string => {
-    if (withTime) {
-        return formatInTimeZone(
-            getDateTime(value) || "",
-            formatConf.forceTZ || !tz ? formatConf.timeZone : tz,
-            datetimeformat || formatConf.dateTime
-        );
+    const dateVal = getDateTime(value);
+    try {
+        if (withTime) {
+            return formatInTimeZone(
+                dateVal || "",
+                formatConf.forceTZ || !tz ? formatConf.timeZone : tz,
+                datetimeformat || formatConf.dateTime
+            );
+        }
+        return format(dateVal || 0, datetimeformat || formatConf.date);
+    } catch (e) {
+        console.warn("Invalid date format:", (e as Error).message || e);
+        return `${dateVal}`;
     }
-    return format(getDateTime(value) || 0, datetimeformat || formatConf.date);
 };
 
 export const getNumberString = (value: number, numberformat: string | undefined, formatConf: FormatConfig): string => {
@@ -121,7 +127,7 @@ export const getNumberString = (value: number, numberformat: string | undefined,
             ? sprintf(numberformat || formatConf.number, value)
             : value.toLocaleString();
     } catch (e) {
-        console.warn("getNumberString: " + (e as Error).message || e);
+        console.warn("Invalid number format:", (e as Error).message || e);
         return (
             (typeof value === "number" && value.toLocaleString()) ||
             (typeof value === "string" && (value as string)) ||

+ 4 - 5
frontend/taipy/src/ScenarioViewer.tsx

@@ -366,7 +366,6 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
             }
         }
         setValid(!!sc);
-        // setSubmissionStatus(0);
         setScenario((oldSc) => (oldSc === sc ? oldSc : sc ? (deepEqual(oldSc, sc) ? oldSc : sc) : invalidScenario));
     }, [props.scenario, props.defaultScenario]);
 
@@ -579,7 +578,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], "", true]]), []);
 
     // Submission status
-    const [submissionStatus, setSubmissionStatus] = useState(0);
+    const [submissionStatus, setSubmissionStatus] = useState(-1);
 
     // on scenario change
     useEffect(() => {
@@ -594,10 +593,10 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     useEffect(() => {
         const ids = props.coreChanged?.scenario;
         if (typeof ids === "string" ? ids === scId : Array.isArray(ids) ? ids.includes(scId) : ids) {
-            props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true));
-            if (props.coreChanged?.submission !== undefined) {
+            if (typeof props.coreChanged?.submission === "number") {
                 setSubmissionStatus(props.coreChanged?.submission as number);
             }
+            props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true));
         }
     }, [props.coreChanged, props.updateVarName, id, module, dispatch, scId]);
 
@@ -629,7 +628,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         sx={ChipSx}
                                     />
                                 ) : null}
-                                {submissionStatus ? <StatusChip status={submissionStatus} sx={ChipSx} /> : null}
+                                {submissionStatus > -1 ? <StatusChip status={submissionStatus} sx={ChipSx} /> : null}
                             </Grid>
                             <Grid item>
                                 {showSubmit ? (

+ 2 - 1
frontend/taipy/src/StatusChip.tsx

@@ -3,7 +3,8 @@ import { SxProps, Theme } from "@mui/material";
 import Chip from "@mui/material/Chip";
 
 export enum Status {
-    SUBMITTED = 1,
+    SUBMITTED = 0,
+    UNDEFINED = 1,
     BLOCKED = 2,
     PENDING = 3,
     RUNNING = 4,

+ 1 - 0
taipy/gui/_renderers/builder.py

@@ -946,6 +946,7 @@ class _Builder:
 
             attributes (list(tuple)): The list of attributes as (property name, property type, default value).
         """
+        attributes.append(("id",)) # Every element should have an id attribute
         for attr in attributes:
             if not isinstance(attr, tuple):
                 attr = (attr,)

+ 0 - 27
taipy/gui/_renderers/factory.py

@@ -78,7 +78,6 @@ class _Factory:
         .set_value_and_default(with_update=False)
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -90,7 +89,6 @@ class _Factory:
         .set_value_and_default(with_update=True, with_default=False, var_type=PropertyType.data)
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -107,7 +105,6 @@ class _Factory:
         .set_value_and_default(with_default=False, var_type=PropertyType.data)
         .set_attributes(
             [
-                ("id",),
                 ("title",),
                 ("width", PropertyType.string_or_number),
                 ("height", PropertyType.string_or_number),
@@ -140,7 +137,6 @@ class _Factory:
         .set_attributes(
             [
                 ("with_time", PropertyType.boolean),
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -160,7 +156,6 @@ class _Factory:
         .set_attributes(
             [
                 ("with_time", PropertyType.boolean),
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
@@ -181,7 +176,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page",),
                 ("title",),
                 ("on_action", PropertyType.function),
@@ -201,7 +195,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page",),
                 ("expanded", PropertyType.dynamic_boolean, True, True, False),
                 ("hover_text", PropertyType.dynamic_string),
@@ -218,7 +211,6 @@ class _Factory:
         ._set_content("content", image=False)
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("render", PropertyType.dynamic_boolean, True),
@@ -238,7 +230,6 @@ class _Factory:
         ._set_file_content()
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("multiple", PropertyType.boolean, False),
@@ -258,7 +249,6 @@ class _Factory:
         ._set_content("content")
         .set_attributes(
             [
-                ("id",),
                 ("on_action", PropertyType.function),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("width",),
@@ -275,7 +265,6 @@ class _Factory:
         .set_value_and_default(with_update=False, native_type=True)
         .set_attributes(
             [
-                ("id",),
                 ("min", PropertyType.number),
                 ("max", PropertyType.number),
                 ("value", PropertyType.dynamic_number),
@@ -296,7 +285,6 @@ class _Factory:
         ._set_propagate()
         .set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
                 ("on_change", PropertyType.function),
@@ -314,7 +302,6 @@ class _Factory:
         .set_value_and_default(with_default=False)
         .set_attributes(
             [
-                ("id",),
                 ("columns[mobile]",),
                 ("gap",),
             ]
@@ -325,7 +312,6 @@ class _Factory:
         .set_value_and_default(default_val="Log-in")
         .set_attributes(
             [
-                ("id",),
                 ("message", PropertyType.dynamic_string),
                 ("on_action", PropertyType.function, "on_login"),
             ]
@@ -338,7 +324,6 @@ class _Factory:
         )
         .set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("label",),
                 ("width",),
@@ -359,7 +344,6 @@ class _Factory:
         .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True)
         .set_attributes(
             [
-                ("id",),
                 ("title",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("layout", PropertyType.dynamic_dict),
@@ -383,7 +367,6 @@ class _Factory:
             gui=gui, control_type=control_type, element_name="NavBar", attributes=attrs, default_value=None
         ).set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
                 ("lov", PropertyType.single_lov),
@@ -401,7 +384,6 @@ class _Factory:
         ._set_propagate()
         .set_attributes(
             [
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
                 ("on_change", PropertyType.function),
@@ -417,7 +399,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page",),
                 ("anchor", PropertyType.string, "left"),
                 ("on_close", PropertyType.function),
@@ -436,7 +417,6 @@ class _Factory:
         ._set_partial()  # partial should be set before page
         .set_attributes(
             [
-                ("id",),
                 ("page", PropertyType.dynamic_string),
                 ("render", PropertyType.dynamic_boolean, True),
                 ("height", PropertyType.dynamic_string),
@@ -454,7 +434,6 @@ class _Factory:
                 ("filter", PropertyType.boolean),
                 ("height", PropertyType.string_or_number),
                 ("hover_text", PropertyType.dynamic_string),
-                ("id",),
                 ("value_by_id", PropertyType.boolean),
                 ("multiple", PropertyType.boolean),
                 ("width", PropertyType.string_or_number),
@@ -478,7 +457,6 @@ class _Factory:
                 ("active", PropertyType.dynamic_boolean, True),
                 ("height",),
                 ("hover_text", PropertyType.dynamic_string),
-                ("id",),
                 ("value_by_id", PropertyType.boolean),
                 ("max", PropertyType.number, 100),
                 ("min", PropertyType.number, 0),
@@ -503,7 +481,6 @@ class _Factory:
         .set_value_and_default(with_update=False)
         .set_attributes(
             [
-                ("id",),
                 ("without_close", PropertyType.boolean, False),
                 ("hover_text", PropertyType.dynamic_string),
             ]
@@ -524,7 +501,6 @@ class _Factory:
                 ("auto_loading", PropertyType.boolean),
                 ("width", PropertyType.string_or_number, "100%"),
                 ("height", PropertyType.string_or_number, "80vh"),
-                ("id",),
                 ("active", PropertyType.dynamic_boolean, True),
                 ("editable", PropertyType.dynamic_boolean, True),
                 ("on_edit", PropertyType.function),
@@ -552,7 +528,6 @@ class _Factory:
         .set_attributes(
             [
                 ("format",),
-                ("id",),
                 ("hover_text", PropertyType.dynamic_string),
                 ("raw", PropertyType.boolean, False),
                 ("mode",),
@@ -566,7 +541,6 @@ class _Factory:
             [
                 ("active", PropertyType.dynamic_boolean, True),
                 ("hover_text", PropertyType.dynamic_string),
-                ("id",),
                 ("label",),
                 ("value_by_id", PropertyType.boolean),
                 ("unselected_value", PropertyType.string, ""),
@@ -592,7 +566,6 @@ class _Factory:
                 ("filter", PropertyType.boolean),
                 ("hover_text", PropertyType.dynamic_string),
                 ("height", PropertyType.string_or_number),
-                ("id",),
                 ("value_by_id", PropertyType.boolean),
                 ("multiple", PropertyType.boolean),
                 ("width", PropertyType.string_or_number),

+ 19 - 21
taipy/gui_core/_context.py

@@ -119,10 +119,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         else None
                     )
                     if sequence and hasattr(sequence, "parent_ids") and sequence.parent_ids:  # type: ignore
-                        self.gui._broadcast(
-                            _GuiCoreContext._CORE_CHANGED_NAME,
-                            {"scenario": list(sequence.parent_ids)},  # type: ignore
-                        )
+                        self.broadcast_core_changed({"scenario": list(sequence.parent_ids)})
             except Exception as e:
                 _warn(f"Access to sequence {event.entity_id} failed", e)
         elif event.entity_type == EventEntityType.JOB:
@@ -133,25 +130,26 @@ class _GuiCoreContext(CoreEventConsumerBase):
         elif event.entity_type == EventEntityType.DATA_NODE:
             with self.lock:
                 self.data_nodes_by_owner = None
-            self.gui._broadcast(
-                _GuiCoreContext._CORE_CHANGED_NAME,
-                {"datanode": event.entity_id if event.operation != EventOperation.DELETION else True},
+            self.broadcast_core_changed(
+                {"datanode": event.entity_id if event.operation != EventOperation.DELETION else True}
             )
 
+    def broadcast_core_changed(self, payload: t.Dict[str, t.Any], client_id: t.Optional[str] = None):
+        self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, payload, client_id)
+
     def scenario_refresh(self, scenario_id: t.Optional[str]):
         with self.lock:
             self.scenario_by_cycle = None
             self.data_nodes_by_owner = None
-        self.gui._broadcast(
-            _GuiCoreContext._CORE_CHANGED_NAME,
-            {"scenario": scenario_id or True},
-        )
+        self.broadcast_core_changed({"scenario": scenario_id or True})
 
     def submission_status_callback(self, submission_id: t.Optional[str] = None, event: t.Optional[Event] = None):
         if not submission_id or not is_readable(t.cast(SubmissionId, submission_id)):
             return
         submission = None
         new_status = None
+        payload: t.Optional[t.Dict[str, t.Any]] = None
+        client_id: t.Optional[str] = None
         try:
             last_status = self.client_submission.get(submission_id)
             if not last_status:
@@ -161,6 +159,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             if not submission or not submission.entity_id:
                 return
 
+            payload = {}
             new_status = t.cast(SubmissionStatus, submission.submission_status)
 
             client_id = submission.properties.get("client_id")
@@ -176,7 +175,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                             if job.is_pending()
                             else None
                         )
-                    self.gui._broadcast(_GuiCoreContext._CORE_CHANGED_NAME, {"tasks": running_tasks}, client_id)
+                    payload.update(tasks=running_tasks)
 
                     if last_status != new_status:
                         # callback
@@ -210,15 +209,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
             _warn(f"Submission ({submission_id}) is not available", e)
 
         finally:
-            entity_id = submission.entity_id if submission else None
-            self.gui._broadcast(
-                _GuiCoreContext._CORE_CHANGED_NAME,
-                {
-                    "jobs": True,
-                    "scenario": entity_id or False,
-                    "submission": new_status.value if new_status else None,
-                },
-            )
+            if payload is not None:
+                payload.update(jobs=True)
+                entity_id = submission.entity_id if submission else None
+                if entity_id:
+                    payload.update(scenario=entity_id)
+                    if new_status:
+                        payload.update(submission=new_status.value)
+                self.broadcast_core_changed(payload, client_id)
 
     def no_change_adapter(self, entity: t.List):
         return entity