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>; +};
