You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by pi...@apache.org on 2025/04/01 15:05:46 UTC

(airflow) branch main updated: UI Alerts on Dashboard (#48450)

This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 3ec912eb8cd UI Alerts on Dashboard (#48450)
3ec912eb8cd is described below

commit 3ec912eb8cdee9fa5e74466cdd6979c0e8fc8972
Author: Aritra Basu <24...@users.noreply.github.com>
AuthorDate: Tue Apr 1 20:35:39 2025 +0530

    UI Alerts on Dashboard (#48450)
    
    * UI Alerts on Dashboard
    closes: #46751
    
    * Updated tests and removed unused params
    
    * Display markdown
    
    * Default to open
    
    * resolved comments
    
    * Made category required
---
 .../src/airflow/api_fastapi/common/types.py        |  9 +++-
 .../api_fastapi/core_api/datamodels/ui/config.py   |  3 ++
 .../api_fastapi/core_api/openapi/v1-generated.yaml | 24 +++++++++
 .../api_fastapi/core_api/routes/ui/config.py       |  3 +-
 airflow-core/src/airflow/settings.py               | 15 ++++++
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 26 ++++++++++
 .../airflow/ui/openapi-gen/requests/types.gen.ts   | 11 ++++
 .../airflow/ui/src/pages/Dashboard/Dashboard.tsx   | 58 +++++++++++++++++-----
 .../api_fastapi/core_api/routes/ui/test_config.py  |  1 +
 9 files changed, 135 insertions(+), 15 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/types.py b/airflow-core/src/airflow/api_fastapi/common/types.py
index 6f5a975722d..fa1a1d904ff 100644
--- a/airflow-core/src/airflow/api_fastapi/common/types.py
+++ b/airflow-core/src/airflow/api_fastapi/common/types.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 from dataclasses import dataclass
 from datetime import timedelta
 from enum import Enum
-from typing import Annotated
+from typing import Annotated, Literal
 
 from pydantic import (
     AfterValidator,
@@ -96,3 +96,10 @@ class MenuItem(Enum):
     PROVIDERS = "Providers"
     VARIABLES = "Variables"
     XCOMS = "XComs"
+
+
+class UIAlert(BaseModel):
+    """Optional alert to be shown at the top of the page."""
+
+    text: str
+    category: Literal["info", "warning", "error"]
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py
index ad2b25c21f4..e7061d2b09b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py
@@ -18,6 +18,8 @@ from __future__ import annotations
 
 from pydantic import BaseModel
 
+from airflow.api_fastapi.common.types import UIAlert
+
 
 class ConfigResponse(BaseModel):
     """configuration serializer."""
@@ -41,3 +43,4 @@ class ConfigResponse(BaseModel):
     audit_view_included_events: str
     test_connection: str
     state_color_mapping: dict
+    dashboard_alert: list[UIAlert]
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index cba60768b27..92298fbc646 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -8220,6 +8220,11 @@ components:
           additionalProperties: true
           type: object
           title: State Color Mapping
+        dashboard_alert:
+          items:
+            $ref: '#/components/schemas/UIAlert'
+          type: array
+          title: Dashboard Alert
       type: object
       required:
       - navbar_color
@@ -8241,6 +8246,7 @@ components:
       - audit_view_included_events
       - test_connection
       - state_color_mapping
+      - dashboard_alert
       title: ConfigResponse
       description: configuration serializer.
     ConfigSection:
@@ -11549,6 +11555,24 @@ components:
       - latest_triggerer_heartbeat
       title: TriggererInfoResponse
       description: Triggerer info serializer for responses.
+    UIAlert:
+      properties:
+        text:
+          type: string
+          title: Text
+        category:
+          type: string
+          enum:
+          - info
+          - warning
+          - error
+          title: Category
+      type: object
+      required:
+      - text
+      - category
+      title: UIAlert
+      description: Optional alert to be shown at the top of the page.
     ValidationError:
       properties:
         loc:
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py
index 23ac1015784..8fe4183a681 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py
@@ -25,7 +25,7 @@ from airflow.api_fastapi.core_api.datamodels.ui.config import ConfigResponse
 from airflow.api_fastapi.core_api.openapi.exceptions import create_openapi_http_exception_doc
 from airflow.api_fastapi.core_api.security import requires_access_configuration
 from airflow.configuration import conf
-from airflow.settings import STATE_COLORS
+from airflow.settings import DASHBOARD_UIALERTS, STATE_COLORS
 
 config_router = AirflowRouter(tags=["Config"])
 
@@ -64,6 +64,7 @@ def get_configs() -> ConfigResponse:
         "audit_view_excluded_events": conf.get("webserver", "audit_view_excluded_events", fallback=""),
         "test_connection": conf.get("core", "test_connection", fallback="Disabled"),
         "state_color_mapping": STATE_COLORS,
+        "dashboard_alert": DASHBOARD_UIALERTS,
     }
 
     config.update({key: value for key, value in additional_config.items()})
diff --git a/airflow-core/src/airflow/settings.py b/airflow-core/src/airflow/settings.py
index 48b74ef1b4e..935ddc1f62f 100644
--- a/airflow-core/src/airflow/settings.py
+++ b/airflow-core/src/airflow/settings.py
@@ -46,6 +46,8 @@ from airflow.utils.timezone import local_timezone, parse_timezone, utc
 if TYPE_CHECKING:
     from sqlalchemy.engine import Engine
 
+    from airflow.api_fastapi.common.types import UIAlert
+
 log = logging.getLogger(__name__)
 
 try:
@@ -128,6 +130,19 @@ STATE_COLORS = {
     "upstream_failed": "orange",
 }
 
+# Display alerts on the dashboard
+# Useful for warning about setup issues or announcing changes to end users
+# List of UIAlerts, which allows for specifying the content and category
+# message to be shown. For example:
+#   from airflow.api_fastapi.common.types import UIAlert
+#
+#   DASHBOARD_UIALERTS = [
+#       UIAlert(text="Welcome to Airflow", category="info"),
+#       UIAlert(text="Upgrade tomorrow [help](https://www.example.com)", category="warning"), #With markdown support
+#   ]
+#
+DASHBOARD_UIALERTS: list[UIAlert] = []
+
 
 @functools.cache
 def _get_rich_console(file):
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 75fee0c59e2..8fb6b1f89dd 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -1229,6 +1229,13 @@ export const $ConfigResponse = {
       type: "object",
       title: "State Color Mapping",
     },
+    dashboard_alert: {
+      items: {
+        $ref: "#/components/schemas/UIAlert",
+      },
+      type: "array",
+      title: "Dashboard Alert",
+    },
   },
   type: "object",
   required: [
@@ -1251,6 +1258,7 @@ export const $ConfigResponse = {
     "audit_view_included_events",
     "test_connection",
     "state_color_mapping",
+    "dashboard_alert",
   ],
   title: "ConfigResponse",
   description: "configuration serializer.",
@@ -6428,6 +6436,24 @@ export const $TriggererInfoResponse = {
   description: "Triggerer info serializer for responses.",
 } as const;
 
+export const $UIAlert = {
+  properties: {
+    text: {
+      type: "string",
+      title: "Text",
+    },
+    category: {
+      type: "string",
+      enum: ["info", "warning", "error"],
+      title: "Category",
+    },
+  },
+  type: "object",
+  required: ["text", "category"],
+  title: "UIAlert",
+  description: "Optional alert to be shown at the top of the page.",
+} as const;
+
 export const $ValidationError = {
   properties: {
     loc: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 361dbee0f56..a1e11ea6581 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -415,6 +415,7 @@ export type ConfigResponse = {
   state_color_mapping: {
     [key: string]: unknown;
   };
+  dashboard_alert: Array<UIAlert>;
 };
 
 /**
@@ -1565,6 +1566,16 @@ export type TriggererInfoResponse = {
   latest_triggerer_heartbeat: string | null;
 };
 
+/**
+ * Optional alert to be shown at the top of the page.
+ */
+export type UIAlert = {
+  text: string;
+  category: "info" | "warning" | "error";
+};
+
+export type category = "info" | "warning" | "error";
+
 export type ValidationError = {
   loc: Array<string | number>;
   msg: string;
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
index 39131f1969c..5dba0f697cc 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
@@ -16,23 +16,55 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Heading } from "@chakra-ui/react";
+import { Box, Heading, VStack } from "@chakra-ui/react";
+
+import type { UIAlert } from "openapi/requests/types.gen";
+import ReactMarkdown from "src/components/ReactMarkdown";
+import { Accordion, Alert } from "src/components/ui";
+import { useConfig } from "src/queries/useConfig";
 
 import { Health } from "./Health";
 import { HistoricalMetrics } from "./HistoricalMetrics";
 import { Stats } from "./Stats";
 
-export const Dashboard = () => (
-  <Box>
-    <Heading mb={4}>Welcome</Heading>
+export const Dashboard = () => {
+  const alerts = useConfig("dashboard_alert") as Array<UIAlert>;
+
+  return (
     <Box>
-      <Health />
-    </Box>
-    <Box mt={5}>
-      <Stats />
-    </Box>
-    <Box mt={5}>
-      <HistoricalMetrics />
+      <VStack alignItems="start">
+        {alerts.length > 0 ? (
+          <Accordion.Root collapsible defaultValue={["ui_alerts"]}>
+            <Accordion.Item key="ui_alerts" value="ui_alerts">
+              {alerts.map((alert: UIAlert, index) =>
+                index === 0 ? (
+                  <Accordion.ItemTrigger key={alert.text}>
+                    <Alert status={alert.category}>
+                      <ReactMarkdown>{alert.text}</ReactMarkdown>
+                    </Alert>
+                  </Accordion.ItemTrigger>
+                ) : (
+                  <Accordion.ItemContent key={alert.text} paddingRight="8">
+                    <Alert status={alert.category}>
+                      <ReactMarkdown>{alert.text}</ReactMarkdown>
+                    </Alert>
+                  </Accordion.ItemContent>
+                ),
+              )}
+            </Accordion.Item>
+          </Accordion.Root>
+        ) : undefined}
+        <Heading mb={4}>Welcome</Heading>
+      </VStack>
+      <Box>
+        <Health />
+      </Box>
+      <Box mt={5}>
+        <Stats />
+      </Box>
+      <Box mt={5}>
+        <HistoricalMetrics />
+      </Box>
     </Box>
-  </Box>
-);
+  );
+};
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
index 273aca72811..2c8f2e68707 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
@@ -55,6 +55,7 @@ mock_config_response = {
         "up_for_retry": "gold",
         "upstream_failed": "orange",
     },
+    "dashboard_alert": [],
 }