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

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit c0025ae3f60eba8e6a3ead3625ae2fafba77250a
Author: Guan Ming(Wesley) Chiu <[email protected]>
AuthorDate: Thu Apr 24 01:09:08 2025 +0800

    Add count to Stats Cards in Dashboard (#49519)
    
    * AIP-38: enhence stats in dashboard
    
    * fix: use semantic tokens
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    
    * fix: apply suggestions from code review
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    
    * fix: replace flex with hstack
    
    * fix: add missing icon
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    
    ---------
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    Co-authored-by: Brent Bovenzi <[email protected]>
    (cherry picked from commit 86cdef22b11e67313e03b61b63e57cc878dad470)
---
 .../airflow/ui/src/pages/Dashboard/Dashboard.tsx   |  14 +--
 .../src/pages/Dashboard/Stats/DAGImportErrors.tsx  |  68 ++++++--------
 .../src/pages/Dashboard/Stats/DagFilterButton.tsx  |  44 ---------
 .../airflow/ui/src/pages/Dashboard/Stats/Stats.tsx | 103 ++++++++++++++++-----
 .../ui/src/pages/Dashboard/Stats/StatsCard.tsx     |  80 ++++++++++++++++
 5 files changed, 195 insertions(+), 114 deletions(-)

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 5dba0f697cc..7d096275b2c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Dashboard.tsx
@@ -31,7 +31,7 @@ export const Dashboard = () => {
   const alerts = useConfig("dashboard_alert") as Array<UIAlert>;
 
   return (
-    <Box>
+    <Box px={4}>
       <VStack alignItems="start">
         {alerts.length > 0 ? (
           <Accordion.Root collapsible defaultValue={["ui_alerts"]}>
@@ -54,15 +54,17 @@ export const Dashboard = () => {
             </Accordion.Item>
           </Accordion.Root>
         ) : undefined}
-        <Heading mb={4}>Welcome</Heading>
+        <Heading mb={2} size="2xl">
+          Welcome,
+        </Heading>
       </VStack>
       <Box>
-        <Health />
-      </Box>
-      <Box mt={5}>
         <Stats />
       </Box>
-      <Box mt={5}>
+      <Box mt={8}>
+        <Health />
+      </Box>
+      <Box mt={8}>
         <HistoricalMetrics />
       </Box>
     </Box>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx
index 0f90d6638d3..6a4a8350953 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx
@@ -16,22 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Text, Button, useDisclosure, Skeleton } from "@chakra-ui/react";
-import { FiChevronRight } from "react-icons/fi";
+import { Box, Button, Skeleton, useDisclosure } from "@chakra-ui/react";
 import { LuFileWarning } from "react-icons/lu";
 
-import { useImportErrorServiceGetImportErrors } from "openapi/queries";
+import { useImportErrorServiceGetImportErrors } from "openapi/queries/queries";
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { StateBadge } from "src/components/StateBadge";
 import { pluralize } from "src/utils";
 
 import { DAGImportErrorsModal } from "./DAGImportErrorsModal";
+import { StatsCard } from "./StatsCard";
 
 export const DAGImportErrors = ({ iconOnly = false }: { readonly iconOnly?: 
boolean }) => {
   const { onClose, onOpen, open } = useDisclosure();
 
   const { data, error, isLoading } = useImportErrorServiceGetImportErrors();
-
   const importErrorsCount = data?.total_entries ?? 0;
   const importErrors = data?.import_errors ?? [];
 
@@ -39,44 +38,35 @@ export const DAGImportErrors = ({ iconOnly = false }: { 
readonly iconOnly?: bool
     return <Skeleton height="9" width="225px" />;
   }
 
+  if (importErrorsCount === 0) {
+    return undefined;
+  }
+
   return (
-    <Box alignItems="center" display="flex" maxH="10px">
+    <Box alignItems="center" display="flex">
       <ErrorAlert error={error} />
-      {importErrorsCount > 0 && (
-        <>
-          {iconOnly ? (
-            <StateBadge
-              as={Button}
-              colorPalette="failed"
-              height={7}
-              onClick={onOpen}
-              title={pluralize("Dag Import Error", importErrorsCount)}
-            >
-              <LuFileWarning size="0.5rem" />
-              {importErrorsCount}
-            </StateBadge>
-          ) : (
-            <Button
-              alignItems="center"
-              borderRadius="md"
-              display="flex"
-              gap={2}
-              onClick={onOpen}
-              variant="outline"
-            >
-              <StateBadge colorPalette="failed">
-                <LuFileWarning />
-                {importErrorsCount}
-              </StateBadge>
-              <Box alignItems="center" display="flex" gap={1}>
-                <Text fontWeight="bold">Dag Import Errors</Text>
-                <FiChevronRight />
-              </Box>
-            </Button>
-          )}
-          <DAGImportErrorsModal importErrors={importErrors} onClose={onClose} 
open={open} />
-        </>
+      {iconOnly ? (
+        <StateBadge
+          as={Button}
+          colorPalette="failed"
+          height={7}
+          onClick={onOpen}
+          title={pluralize("Dag Import Error", importErrorsCount)}
+        >
+          <LuFileWarning size="0.5rem" />
+          {importErrorsCount}
+        </StateBadge>
+      ) : (
+        <StatsCard
+          colorScheme="failed"
+          count={importErrorsCount}
+          icon={<LuFileWarning />}
+          isLoading={isLoading}
+          label="Dag Import Errors"
+          onClick={onOpen}
+        />
       )}
+      <DAGImportErrorsModal importErrors={importErrors} onClose={onClose} 
open={open} />
     </Box>
   );
 };
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagFilterButton.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagFilterButton.tsx
deleted file mode 100644
index 0ac6490ae79..00000000000
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagFilterButton.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/*!
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 { Box, Text, Button, Badge, type BadgeProps } from "@chakra-ui/react";
-import { FiChevronRight } from "react-icons/fi";
-import { Link as RouterLink } from "react-router-dom";
-
-import { capitalize } from "src/utils";
-
-// TODO: Add badge count once API is available
-
-type Props = {
-  readonly filter: string;
-  readonly link: string;
-} & BadgeProps;
-
-export const DagFilterButton = ({ children, filter, link, ...rest }: Props) => 
(
-  <RouterLink to={link}>
-    <Button alignItems="center" borderRadius="md" display="flex" gap={2} 
variant="outline">
-      <Box alignItems="center" display="flex" gap={1}>
-        <Badge borderRadius="full" p={1} variant="solid" {...rest}>
-          {children}
-        </Badge>
-        <Text fontWeight="bold">{capitalize(filter)} Dags</Text>
-        <FiChevronRight />
-      </Box>
-    </Button>
-  </RouterLink>
-);
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
index 70fe310a36e..085a348fa7a 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
@@ -19,30 +19,83 @@
 import { Box, Flex, Heading, HStack } from "@chakra-ui/react";
 import { FiClipboard, FiZap } from "react-icons/fi";
 
-import { StateIcon } from "src/components/StateIcon";
+import { useDagServiceGetDags } from "openapi/queries";
 
 import { DAGImportErrors } from "./DAGImportErrors";
-import { DagFilterButton } from "./DagFilterButton";
-
-export const Stats = () => (
-  <Box>
-    <Flex color="fg.muted" my={2}>
-      <FiClipboard />
-      <Heading ml={1} size="xs">
-        Links
-      </Heading>
-    </Flex>
-    <HStack>
-      <DagFilterButton colorPalette="failed" filter="failed" 
link="dags?last_dag_run_state=failed">
-        <StateIcon state="failed" />
-      </DagFilterButton>
-      <DAGImportErrors />
-      <DagFilterButton colorPalette="running" filter="running" 
link="dags?last_dag_run_state=running">
-        <StateIcon state="running" />
-      </DagFilterButton>
-      <DagFilterButton colorPalette="blue" filter="active" 
link="dags?paused=false">
-        <FiZap />
-      </DagFilterButton>
-    </HStack>
-  </Box>
-);
+import { StatsCard } from "./StatsCard";
+
+export const Stats = () => {
+  const { data: activeDagsData, isLoading: isActiveDagsLoading } = 
useDagServiceGetDags({
+    paused: false,
+  });
+
+  const { data: failedDagsData, isLoading: isFailedDagsLoading } = 
useDagServiceGetDags({
+    lastDagRunState: "failed",
+  });
+
+  const { data: queuedDagsData, isLoading: isQueuedDagsLoading } = 
useDagServiceGetDags({
+    lastDagRunState: "queued",
+  });
+
+  const { data: runningDagsData, isLoading: isRunningDagsLoading } = 
useDagServiceGetDags({
+    lastDagRunState: "running",
+  });
+
+  const activeDagsCount = activeDagsData?.total_entries ?? 0;
+  const failedDagsCount = failedDagsData?.total_entries ?? 0;
+  const queuedDagsCount = queuedDagsData?.total_entries ?? 0;
+  const runningDagsCount = runningDagsData?.total_entries ?? 0;
+
+  return (
+    <Box>
+      <Flex alignItems="center" color="fg.muted" my={2}>
+        <FiClipboard />
+        <Heading ml={1} size="xs">
+          Stats
+        </Heading>
+      </Flex>
+
+      <HStack columns={{ base: 1, lg: 5, md: 3 }} gap={4}>
+        <StatsCard
+          colorScheme="failed"
+          count={failedDagsCount}
+          isLoading={isFailedDagsLoading}
+          label="Failed dags"
+          link="dags?last_dag_run_state=failed"
+          state="failed"
+        />
+
+        <DAGImportErrors />
+
+        {queuedDagsCount > 0 ? (
+          <StatsCard
+            colorScheme="queued"
+            count={queuedDagsCount}
+            isLoading={isQueuedDagsLoading}
+            label="Queued dags"
+            link="dags?last_dag_run_state=queued"
+            state="queued"
+          />
+        ) : undefined}
+
+        <StatsCard
+          colorScheme="running"
+          count={runningDagsCount}
+          isLoading={isRunningDagsLoading}
+          label="Running dags"
+          link="dags?last_dag_run_state=running"
+          state="running"
+        />
+
+        <StatsCard
+          colorScheme="blue"
+          count={activeDagsCount}
+          icon={<FiZap />}
+          isLoading={isActiveDagsLoading}
+          label="Active dags"
+          link="dags?paused=false"
+        />
+      </HStack>
+    </Box>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/StatsCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/StatsCard.tsx
new file mode 100644
index 00000000000..d49c7bba124
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/StatsCard.tsx
@@ -0,0 +1,80 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 { Box, HStack, Skeleton, Text } from "@chakra-ui/react";
+import { FiChevronRight } from "react-icons/fi";
+import { Link as RouterLink } from "react-router-dom";
+
+import type { TaskInstanceState } from "openapi/requests/types.gen";
+import { StateBadge } from "src/components/StateBadge";
+import { capitalize } from "src/utils";
+
+export const StatsCard = ({
+  colorScheme,
+  count,
+  icon,
+  isLoading = false,
+  label,
+  link,
+  onClick,
+  state,
+}: {
+  readonly colorScheme: string;
+  readonly count: number;
+  readonly icon?: React.ReactNode;
+  readonly isLoading?: boolean;
+  readonly label: string;
+  readonly link?: string;
+  readonly onClick?: () => void;
+  readonly state?: TaskInstanceState | null;
+}) => {
+  if (isLoading) {
+    return <Skeleton borderRadius="lg" height="42px" width="175px" />;
+  }
+
+  const content = (
+    <HStack
+      alignItems="center"
+      borderRadius="lg"
+      borderWidth={1}
+      color="fg.emphasized"
+      cursor="pointer"
+      p={2}
+    >
+      <StateBadge colorPalette={colorScheme} mr={2} state={state}>
+        {icon}
+        {count}
+      </StateBadge>
+
+      <Text color="fg" fontSize="sm" fontWeight="bold">
+        {capitalize(label)}
+      </Text>
+      <FiChevronRight size={16} />
+    </HStack>
+  );
+
+  if (onClick) {
+    return (
+      <Box as="button" onClick={onClick}>
+        {content}
+      </Box>
+    );
+  }
+
+  return <RouterLink to={link ?? "#"}>{content}</RouterLink>;
+};

Reply via email to