Ver Fonte

Add AlertComponent for displaying alerts in the GUI (#1975)

* Add AlertComponent for displaying alerts in the GUI

* Add AlertComponent in index.ts

* Refactor AlertComponent to handle dynamic message and update dependencies

* Renamed AlertComponent to Notification and updated dependencies

* updated dependencies

* updated viselements.json

* Refactor Notification component to handle dynamic message and add defaultMessage property

* Apply suggestions from code review

Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com>

* Refactor Notification component and add dynamic message handling

- Refactor the Notification component in Taipy to improve code readability and maintainability.
- Add support for dynamic message handling using the useDynamicProperty hook.
- Remove the variant and defaultMessage properties from the Notification component, as they are no longer needed.

Closes #693

* Renamed Notification component to Alert and vice versa and updated dependencies

* Refactor Alert component and update dependencies

* Refactor Alert component to add dynamic rendering capability

* feat: Enhance TaipyAlert with dynamic classNames and dispatch actions

- Added dynamic className handling to TaipyAlert component.
- Implemented dispatching of update actions.
- Fixed issues with severity and variant properties.
- Added unit tests to validate the new behavior, with all tests passing successfully.

* Add Alert.py example with dynamic properties and button to update alert

* Refactor Alert.py example and add package.json

- Refactor Alert.py example to remove unused code and simplify the page structure.
- Add package.json file for frontend/taipy directory from develop branch.

* refactor package.json to match it with develop branch

* Add license headers to Alert components

* Fixed linter issue using ruff

* Refactor Notification component and fix merge issue

* Refactor Notification component and fix issue due to other PR

* Refactor Notification test component to include notificationId in alerts

---------

Co-authored-by: Fabien Lelaquais <86590727+FabienLelaquais@users.noreply.github.com>
Co-authored-by: Fred Lefévère-Laoide <90181748+FredLL-Avaiga@users.noreply.github.com>
Rishi Nayak há 6 meses atrás
pai
commit
3621961930

+ 28 - 0
doc/gui/examples/Alert.py

@@ -0,0 +1,28 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+from taipy.gui import Gui
+
+severity = "error"
+variant = "filled"
+message = "This is an error message."
+
+page = """
+<|{message}|alert|severity={severity}|variant={variant}|>
+"""
+
+if __name__ == "__main__":
+    gui = Gui(page)
+    gui.run(title="Test Alert")

+ 2 - 2
frontend/taipy-gui/src/components/Router.tsx

@@ -34,10 +34,10 @@ import {
     taipyInitialize,
     taipyInitialize,
     taipyReducer,
     taipyReducer,
 } from "../context/taipyReducers";
 } from "../context/taipyReducers";
-import Alert from "./Taipy/Alert";
 import UIBlocker from "./Taipy/UIBlocker";
 import UIBlocker from "./Taipy/UIBlocker";
 import Navigate from "./Taipy/Navigate";
 import Navigate from "./Taipy/Navigate";
 import Menu from "./Taipy/Menu";
 import Menu from "./Taipy/Menu";
+import TaipyNotification from "./Taipy/Notification";
 import GuiDownload from "./Taipy/GuiDownload";
 import GuiDownload from "./Taipy/GuiDownload";
 import ErrorFallback from "../utils/ErrorBoundary";
 import ErrorFallback from "../utils/ErrorBoundary";
 import MainPage from "./pages/MainPage";
 import MainPage from "./pages/MainPage";
@@ -152,7 +152,7 @@ const Router = () => {
                                         ) : null}
                                         ) : null}
                                     </Box>
                                     </Box>
                                     <ErrorBoundary FallbackComponent={ErrorFallback}>
                                     <ErrorBoundary FallbackComponent={ErrorFallback}>
-                                        <Alert alerts={state.alerts} />
+                                        <TaipyNotification alerts={state.alerts} />
                                         <UIBlocker block={state.block} />
                                         <UIBlocker block={state.block} />
                                         <Navigate
                                         <Navigate
                                             to={state.navigateTo}
                                             to={state.navigateTo}

+ 26 - 176
frontend/taipy-gui/src/components/Taipy/Alert.spec.tsx

@@ -12,191 +12,41 @@
  */
  */
 
 
 import React from "react";
 import React from "react";
-import { render, screen, waitFor } from "@testing-library/react";
+import { render } from "@testing-library/react";
 import "@testing-library/jest-dom";
 import "@testing-library/jest-dom";
-import { SnackbarProvider } from "notistack";
+import TaipyAlert from "./Alert";
 
 
-import Alert from "./Alert";
-import { AlertMessage } from "../../context/taipyReducers";
-import userEvent from "@testing-library/user-event";
-
-const defaultMessage = "message";
-const defaultAlerts: AlertMessage[] = [{ atype: "success", message: defaultMessage, system: true, duration: 3000 }];
-const getAlertsWithType = (aType: string) => [{ ...defaultAlerts[0], atype: aType }];
-
-class myNotification {
-    static requestPermission = jest.fn(() => Promise.resolve("granted"));
-    static permission = "granted";
-}
-
-describe("Alert Component", () => {
-    beforeAll(() => {
-        globalThis.Notification = myNotification as unknown as jest.Mocked<typeof Notification>;
-    });
-    beforeEach(() => {
-        jest.clearAllMocks();
-    });
-    it("renders", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={defaultAlerts} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.tagName).toBe("DIV");
-    });
-    it("displays a success alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={defaultAlerts} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-success")).toBeInTheDocument();
-    });
-    it("displays an error alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={getAlertsWithType("error")} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-error")).toBeInTheDocument();
-    });
-    it("displays a warning alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={getAlertsWithType("warning")} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-warning")).toBeInTheDocument();
-    });
-    it("displays an info alert", async () => {
-        const { getByText } = render(
-            <SnackbarProvider>
-                <Alert alerts={getAlertsWithType("info")} />
-            </SnackbarProvider>,
-        );
-        const elt = getByText(defaultMessage);
-        expect(elt.closest(".notistack-MuiContent-info")).toBeInTheDocument();
-    });
-    it("gets favicon URL from document link tags", () => {
-        const link = document.createElement("link");
-        link.rel = "icon";
-        link.href = "/test-icon.png";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='icon']");
-        if (linkElement) {
-            expect(linkElement.getAttribute("href")).toBe("/test-icon.png");
-        } else {
-            expect(true).toBe(false);
-        }
-        document.head.removeChild(link);
-    });
-
-    it("closes alert on close button click", async () => {
-        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false }];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const closeButton = await screen.findByRole("button", { name: /close/i });
-        await userEvent.click(closeButton);
-        await waitFor(() => {
-            const alertMessage = screen.queryByText("Test Alert");
-            expect(alertMessage).not.toBeInTheDocument();
-        });
-    });
-
-    it("Alert disappears when alert type is empty", async () => {
-        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false }];
-        const { rerender } = render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        await screen.findByRole("button", { name: /close/i });
-        const newAlerts = [{ atype: "", message: "Test Alert", duration: 3000, system: false }];
-        rerender(
-            <SnackbarProvider>
-                <Alert alerts={newAlerts} />
-            </SnackbarProvider>,
-        );
-        await waitFor(() => {
-            const alertMessage = screen.queryByText("Test Alert");
-            expect(alertMessage).not.toBeInTheDocument();
-        });
+describe("TaipyAlert Component", () => {
+    it("renders with default properties", () => {
+        const { getByRole } = render(<TaipyAlert message="Default Alert" />);
+        const alert = getByRole("alert");
+        expect(alert).toBeInTheDocument();
+        expect(alert).toHaveClass("MuiAlert-filledError");
     });
     });
 
 
-    it("does nothing when alert is undefined", async () => {
-        render(
-            <SnackbarProvider>
-                <Alert alerts={[]} />
-            </SnackbarProvider>,
-        );
-        expect(Notification.requestPermission).not.toHaveBeenCalled();
+    it("applies the correct severity", () => {
+        const { getByRole } = render(<TaipyAlert message="Warning Alert" severity="warning" />);
+        const alert = getByRole("alert");
+        expect(alert).toBeInTheDocument();
+        expect(alert).toHaveClass("MuiAlert-filledWarning");
     });
     });
 
 
-    it("validates href when rel attribute is 'icon' and href is set", () => {
-        const link = document.createElement("link");
-        link.rel = "icon";
-        link.href = "/test-icon.png";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='icon']");
-        expect(linkElement?.getAttribute("href")).toBe("/test-icon.png");
-        document.head.removeChild(link);
+    it("applies the correct variant", () => {
+        const { getByRole } = render(<TaipyAlert message="Outlined Alert" variant="outlined" />);
+        const alert = getByRole("alert");
+        expect(alert).toBeInTheDocument();
+        expect(alert).toHaveClass("MuiAlert-outlinedError");
     });
     });
 
 
-    it("verifies default favicon for 'icon' rel attribute when href is unset/empty", () => {
-        const link = document.createElement("link");
-        link.rel = "icon";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='icon']");
-        expect(linkElement?.getAttribute("href") || "/favicon.png").toBe("/favicon.png");
-        document.head.removeChild(link);
+    it("does not render if render prop is false", () => {
+        const { queryByRole } = render(<TaipyAlert message="Hidden Alert" render={false} />);
+        const alert = queryByRole("alert");
+        expect(alert).toBeNull();
     });
     });
 
 
-    it("validates href when rel attribute is 'shortcut icon' and href is provided", () => {
-        const link = document.createElement("link");
-        link.rel = "shortcut icon";
-        link.href = "/test-shortcut-icon.png";
-        document.head.appendChild(link);
-        const alerts: AlertMessage[] = [
-            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
-        ];
-        render(
-            <SnackbarProvider>
-                <Alert alerts={alerts} />
-            </SnackbarProvider>,
-        );
-        const linkElement = document.querySelector("link[rel='shortcut icon']");
-        expect(linkElement?.getAttribute("href")).toBe("/test-shortcut-icon.png");
-        document.head.removeChild(link);
+    it("handles dynamic class names", () => {
+        const { getByRole } = render(<TaipyAlert message="Dynamic Alert" className="custom-class" />);
+        const alert = getByRole("alert");
+        expect(alert).toHaveClass("custom-class");
     });
     });
 });
 });

+ 32 - 64
frontend/taipy-gui/src/components/Taipy/Alert.tsx

@@ -11,72 +11,40 @@
  * specific language governing permissions and limitations under the License.
  * specific language governing permissions and limitations under the License.
  */
  */
 
 
-import React, { useCallback, useEffect, useMemo } from "react";
-import { SnackbarKey, useSnackbar, VariantType } from "notistack";
-import IconButton from "@mui/material/IconButton";
-import CloseIcon from "@mui/icons-material/Close";
-import { nanoid } from 'nanoid';
-
-import { AlertMessage, createDeleteAlertAction } from "../../context/taipyReducers";
-import { useDispatch } from "../../utils/hooks";
-
-interface AlertProps {
-    alerts: AlertMessage[];
+import React from "react";
+import Alert from "@mui/material/Alert";
+import { TaipyBaseProps } from "./utils";
+import { useClassNames, useDynamicProperty } from "../../utils/hooks";
+
+interface AlertProps extends TaipyBaseProps {
+    severity?: "error" | "warning" | "info" | "success";
+    message?: string;
+    variant?: "filled" | "outlined";
+    render?: boolean;
+    defaultMessage?: string;
+    defaultSeverity?: string;
+    defaultVariant?: string;
+    defaultRender?: boolean;
 }
 }
 
 
-const Alert = ({ alerts }: AlertProps) => {
-    const alert = alerts.length ? alerts[0] : undefined;
-    const { enqueueSnackbar, closeSnackbar } = useSnackbar();
-    const dispatch = useDispatch();
-
-    const resetAlert = useCallback(
-        (key: SnackbarKey) => () => {
-            closeSnackbar(key);
-        },
-        [closeSnackbar]
-    );
-
-    const notifAction = useCallback(
-        (key: SnackbarKey) => (
-            <IconButton size="small" aria-label="close" color="inherit" onClick={resetAlert(key)}>
-                <CloseIcon fontSize="small" />
-            </IconButton>
-        ),
-        [resetAlert]
+const TaipyAlert = (props: AlertProps) => {
+    const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
+    const render = useDynamicProperty(props.render, props.defaultRender, true);
+    const severity = useDynamicProperty(props.severity, props.defaultSeverity, "error") as
+        | "error"
+        | "warning"
+        | "info"
+        | "success";
+    const variant = useDynamicProperty(props.variant, props.defaultVariant, "filled") as "filled" | "outlined";
+    const message = useDynamicProperty(props.message, props.defaultMessage, "");
+
+    if (!render) return null;
+
+    return (
+        <Alert severity={severity} variant={variant} id={props.id} className={className}>
+            {message}
+        </Alert>
     );
     );
-
-    const faviconUrl = useMemo(() => {
-        const nodeList = document.getElementsByTagName("link");
-        for (let i = 0; i < nodeList.length; i++) {
-            if (nodeList[i].getAttribute("rel") == "icon" || nodeList[i].getAttribute("rel") == "shortcut icon") {
-                return nodeList[i].getAttribute("href") || "/favicon.png";
-            }
-        }
-        return "/favicon.png";
-    }, []);
-
-    useEffect(() => {
-        if (alert) {
-            const notificationId = nanoid(); 
-            if (alert.atype === "") {
-                closeSnackbar(notificationId);  
-            } else {
-                enqueueSnackbar(alert.message, {
-                    variant: alert.atype as VariantType,
-                    action: notifAction,  
-                    autoHideDuration: alert.duration,
-                    key: notificationId,  
-                });
-                alert.system && new Notification(document.title || "Taipy", { body: alert.message, icon: faviconUrl });
-            }
-            dispatch(createDeleteAlertAction(notificationId));
-        }
-    }, [alert, enqueueSnackbar, closeSnackbar, notifAction, faviconUrl, dispatch]);
-    useEffect(() => {
-        alert?.system && window.Notification && Notification.requestPermission();
-    }, [alert?.system]);
-
-    return null;
 };
 };
 
 
-export default Alert;
+export default TaipyAlert;

+ 202 - 0
frontend/taipy-gui/src/components/Taipy/Notification.spec.tsx

@@ -0,0 +1,202 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import React from "react";
+import { render, screen, waitFor } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import { SnackbarProvider } from "notistack";
+
+import Alert from "./Notification";
+import { AlertMessage } from "../../context/taipyReducers";
+import userEvent from "@testing-library/user-event";
+
+const defaultMessage = "message";
+const defaultAlerts: AlertMessage[] = [{ atype: "success", message: defaultMessage, system: true, duration: 3000 }];
+const getAlertsWithType = (aType: string) => [{ ...defaultAlerts[0], atype: aType }];
+
+class myNotification {
+    static requestPermission = jest.fn(() => Promise.resolve("granted"));
+    static permission = "granted";
+}
+
+describe("Alert Component", () => {
+    beforeAll(() => {
+        globalThis.Notification = myNotification as unknown as jest.Mocked<typeof Notification>;
+    });
+    beforeEach(() => {
+        jest.clearAllMocks();
+    });
+    it("renders", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={defaultAlerts} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.tagName).toBe("DIV");
+    });
+    it("displays a success alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={defaultAlerts} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-success")).toBeInTheDocument();
+    });
+    it("displays an error alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={getAlertsWithType("error")} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-error")).toBeInTheDocument();
+    });
+    it("displays a warning alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={getAlertsWithType("warning")} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-warning")).toBeInTheDocument();
+    });
+    it("displays an info alert", async () => {
+        const { getByText } = render(
+            <SnackbarProvider>
+                <Alert alerts={getAlertsWithType("info")} />
+            </SnackbarProvider>,
+        );
+        const elt = getByText(defaultMessage);
+        expect(elt.closest(".notistack-MuiContent-info")).toBeInTheDocument();
+    });
+    it("gets favicon URL from document link tags", () => {
+        const link = document.createElement("link");
+        link.rel = "icon";
+        link.href = "/test-icon.png";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='icon']");
+        if (linkElement) {
+            expect(linkElement.getAttribute("href")).toBe("/test-icon.png");
+        } else {
+            expect(true).toBe(false);
+        }
+        document.head.removeChild(link);
+    });
+
+    it("closes alert on close button click", async () => {
+        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false }];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const closeButton = await screen.findByRole("button", { name: /close/i });
+        await userEvent.click(closeButton);
+        await waitFor(() => {
+            const alertMessage = screen.queryByText("Test Alert");
+            expect(alertMessage).not.toBeInTheDocument();
+        });
+    });
+
+    it("Alert disappears when alert type is empty", async () => {
+        const alerts = [{ atype: "success", message: "Test Alert", duration: 3000, system: false, notificationId: "aNotificationId" }];
+        const { rerender } = render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        await screen.findByRole("button", { name: /close/i });
+        const newAlerts = [{ atype: "", message: "Test Alert", duration: 3000, system: false, notificationId: "aNotificationId" }];
+        rerender(
+            <SnackbarProvider>
+                <Alert alerts={newAlerts} />
+            </SnackbarProvider>,
+        );
+        await waitFor(() => {
+            const alertMessage = screen.queryByText("Test Alert");
+            expect(alertMessage).not.toBeInTheDocument();
+        });
+    });
+
+    it("does nothing when alert is undefined", async () => {
+        render(
+            <SnackbarProvider>
+                <Alert alerts={[]} />
+            </SnackbarProvider>,
+        );
+        expect(Notification.requestPermission).not.toHaveBeenCalled();
+    });
+
+    it("validates href when rel attribute is 'icon' and href is set", () => {
+        const link = document.createElement("link");
+        link.rel = "icon";
+        link.href = "/test-icon.png";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='icon']");
+        expect(linkElement?.getAttribute("href")).toBe("/test-icon.png");
+        document.head.removeChild(link);
+    });
+
+    it("verifies default favicon for 'icon' rel attribute when href is unset/empty", () => {
+        const link = document.createElement("link");
+        link.rel = "icon";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='icon']");
+        expect(linkElement?.getAttribute("href") || "/favicon.png").toBe("/favicon.png");
+        document.head.removeChild(link);
+    });
+
+    it("validates href when rel attribute is 'shortcut icon' and href is provided", () => {
+        const link = document.createElement("link");
+        link.rel = "shortcut icon";
+        link.href = "/test-shortcut-icon.png";
+        document.head.appendChild(link);
+        const alerts: AlertMessage[] = [
+            { atype: "success", message: "This is a system alert", system: true, duration: 3000 },
+        ];
+        render(
+            <SnackbarProvider>
+                <Alert alerts={alerts} />
+            </SnackbarProvider>,
+        );
+        const linkElement = document.querySelector("link[rel='shortcut icon']");
+        expect(linkElement?.getAttribute("href")).toBe("/test-shortcut-icon.png");
+        document.head.removeChild(link);
+    });
+});

+ 81 - 0
frontend/taipy-gui/src/components/Taipy/Notification.tsx

@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import React, { useCallback, useEffect, useMemo } from "react";
+import { SnackbarKey, useSnackbar, VariantType } from "notistack";
+import IconButton from "@mui/material/IconButton";
+import CloseIcon from "@mui/icons-material/Close";
+
+import { AlertMessage, createDeleteAlertAction } from "../../context/taipyReducers";
+import { useDispatch } from "../../utils/hooks";
+
+interface NotificationProps {
+    alerts: AlertMessage[];
+}
+
+const TaipyNotification = ({ alerts }: NotificationProps) => {
+    const alert = alerts.length ? alerts[0] : undefined;
+    const { enqueueSnackbar, closeSnackbar } = useSnackbar();
+    const dispatch = useDispatch();
+
+    const resetAlert = useCallback(
+        (key: SnackbarKey) => () => {
+            closeSnackbar(key);
+        },
+        [closeSnackbar]
+    );
+
+    const notifAction = useCallback(
+        (key: SnackbarKey) => (
+            <IconButton size="small" aria-label="close" color="inherit" onClick={resetAlert(key)}>
+                <CloseIcon fontSize="small" />
+            </IconButton>
+        ),
+        [resetAlert]
+    );
+
+    const faviconUrl = useMemo(() => {
+        const nodeList = document.getElementsByTagName("link");
+        for (let i = 0; i < nodeList.length; i++) {
+            if (nodeList[i].getAttribute("rel") == "icon" || nodeList[i].getAttribute("rel") == "shortcut icon") {
+                return nodeList[i].getAttribute("href") || "/favicon.png";
+            }
+        }
+        return "/favicon.png";
+    }, []);
+
+    useEffect(() => {
+        if (alert) {
+            const notificationId = alert.notificationId || "";
+            if (alert.atype === "") {
+                closeSnackbar(notificationId);
+            } else {
+                enqueueSnackbar(alert.message, {
+                    variant: alert.atype as VariantType,
+                    action: notifAction,
+                    autoHideDuration: alert.duration,
+                    key: notificationId,
+                });
+                alert.system && new Notification(document.title || "Taipy", { body: alert.message, icon: faviconUrl });
+            }
+            dispatch(createDeleteAlertAction(notificationId));
+        }
+    }, [alert, enqueueSnackbar, closeSnackbar, notifAction, faviconUrl, dispatch]);
+    useEffect(() => {
+        alert?.system && window.Notification && Notification.requestPermission();
+    }, [alert?.system]);
+
+    return null;
+};
+
+export default TaipyNotification;

+ 3 - 1
frontend/taipy-gui/src/components/Taipy/index.ts

@@ -40,6 +40,7 @@ import Selector from "./Selector";
 import Slider from "./Slider";
 import Slider from "./Slider";
 import StatusList from "./StatusList";
 import StatusList from "./StatusList";
 import Table from "./Table";
 import Table from "./Table";
+import TaipyAlert from "./Alert";
 import TaipyStyle from "./TaipyStyle";
 import TaipyStyle from "./TaipyStyle";
 import Toggle from "./Toggle";
 import Toggle from "./Toggle";
 import TimeSelector from "./TimeSelector";
 import TimeSelector from "./TimeSelector";
@@ -51,6 +52,7 @@ export const getRegisteredComponents = () => {
     if (registeredComponents.TreeView === undefined) {
     if (registeredComponents.TreeView === undefined) {
         Object.entries({
         Object.entries({
             a: Link,
             a: Link,
+            Alert: TaipyAlert,
             Button,
             Button,
             Chat,
             Chat,
             Chart,
             Chart,
@@ -81,7 +83,7 @@ export const getRegisteredComponents = () => {
             Toggle,
             Toggle,
             TreeView,
             TreeView,
             Progress,
             Progress,
-        }).forEach(([name, comp]) => (registeredComponents[name] = comp  as ComponentType));
+        }).forEach(([name, comp]) => (registeredComponents[name] = comp as ComponentType));
         if (window.taipyConfig?.extensions) {
         if (window.taipyConfig?.extensions) {
             Object.entries(window.taipyConfig.extensions).forEach(([libName, elements]) => {
             Object.entries(window.taipyConfig.extensions).forEach(([libName, elements]) => {
                 if (elements && elements.length) {
                 if (elements && elements.length) {

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

@@ -30,6 +30,7 @@ class _Factory:
     __TAIPY_NAME_SPACE = "taipy."
     __TAIPY_NAME_SPACE = "taipy."
 
 
     __CONTROL_DEFAULT_PROP_NAME = {
     __CONTROL_DEFAULT_PROP_NAME = {
+        "alert": "message",
         "button": "label",
         "button": "label",
         "chat": "messages",
         "chat": "messages",
         "chart": "data",
         "chart": "data",
@@ -70,6 +71,21 @@ class _Factory:
     __LIBRARIES: t.Dict[str, t.List["ElementLibrary"]] = {}
     __LIBRARIES: t.Dict[str, t.List["ElementLibrary"]] = {}
 
 
     __CONTROL_BUILDERS = {
     __CONTROL_BUILDERS = {
+        "alert":
+        lambda gui, control_type, attrs: _Builder(
+            gui=gui,
+            control_type=control_type,
+            element_name="Alert",
+            attributes=attrs,
+        )
+        .set_value_and_default(var_type=PropertyType.dynamic_string)
+        .set_attributes(
+            [
+                ("severity", PropertyType.dynamic_string),
+                ("variant", PropertyType.dynamic_string),
+                ("render", PropertyType.dynamic_boolean, True),
+            ]
+        ),
         "button": lambda gui, control_type, attrs: _Builder(
         "button": lambda gui, control_type, attrs: _Builder(
             gui=gui,
             gui=gui,
             control_type=control_type,
             control_type=control_type,

+ 34 - 1
taipy/gui/viselements.json

@@ -1620,6 +1620,39 @@
                 ]
                 ]
             }
             }
         ],
         ],
+        [
+            "alert",  
+            {
+                "inherits": ["shared"],
+                "properties": [
+                    {
+                        "name": "message",
+                        "default_property": true,
+                        "type": "dynamic(str)",
+                        "default_value": "\"\"",
+                        "doc": "The message displayed in the notification. Can be a dynamic string."
+                    },
+                    {
+                        "name": "severity",
+                        "type": "dynamic(str)",
+                        "default_value": "\"error\"",
+                        "doc": "The severity level of the alert. Valid values: \"error\", \"warning\", \"info\", \"success\".\nThe default is \"error\"."
+                    },
+                    {
+                        "name": "variant",
+                        "type": "dynamic(str)",
+                        "default_value": "\"filled\"",
+                        "doc": "The variant of the alert. Valid values: \"filled\", \"outlined\".\nThe default is \"filled\"."
+                    },
+                    {
+                        "name": "render",
+                        "type": "dynamic(bool)",
+                        "default_value": "True",
+                        "doc": "If False, the alert is hidden."
+                    }
+                ]
+            }                       
+        ],
         [
         [
             "status",
             "status",
             {
             {
@@ -1799,7 +1832,7 @@
                     }
                     }
                 ]
                 ]
             }
             }
-        ]
+        ]                                       
     ],
     ],
     "blocks": [
     "blocks": [
         [
         [