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

(airflow) branch main updated: Add icons to task states (#46028)

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

bbovenzi 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 2548731089e Add icons to task states (#46028)
2548731089e is described below

commit 2548731089e4949ab79d7955840071dd6b1a6337
Author: Brent Bovenzi <br...@astronomer.io>
AuthorDate: Sat Jan 25 10:39:21 2025 -0500

    Add icons to task states (#46028)
    
    * Add icons to task states
    
    * Move stateColor to theme
    
    * Fix graph
---
 airflow/ui/src/components/DagRunInfo.tsx           |   2 +-
 airflow/ui/src/components/MetricsBadge.tsx         |   9 +-
 airflow/ui/src/components/StateIcon.tsx            |  70 ++++++++
 airflow/ui/src/components/TaskInstanceTooltip.tsx  |   1 +
 airflow/ui/src/components/ui/Status.tsx            |  26 ++-
 airflow/ui/src/layouts/Details/Graph/Graph.tsx     |   3 +-
 airflow/ui/src/layouts/Details/Graph/TaskNode.tsx  |   9 +-
 airflow/ui/src/main.tsx                            |   5 +-
 airflow/ui/src/pages/Dag/Overview/Overview.tsx     |   5 +-
 airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx        |   4 +-
 airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx  |   3 +-
 airflow/ui/src/pages/DagsList/RecentRuns.tsx       |   3 +-
 .../ui/src/pages/Dashboard/Health/HealthTag.tsx    |  12 +-
 .../Dashboard/HistoricalMetrics/AssetEvents.tsx    |   2 +-
 .../Dashboard/HistoricalMetrics/DagRunMetrics.tsx  |   2 +-
 .../Dashboard/HistoricalMetrics/MetricSection.tsx  |   5 +-
 .../HistoricalMetrics/TaskInstanceMetrics.tsx      |   2 +-
 .../src/pages/Dashboard/Stats/DAGImportErrors.tsx  |   3 +-
 .../src/pages/Dashboard/Stats/DagFilterButton.tsx  |   9 +-
 airflow/ui/src/pages/Dashboard/Stats/Stats.tsx     |  14 +-
 airflow/ui/src/pages/Pools/PoolBar.tsx             |  23 ++-
 airflow/ui/src/theme.ts                            | 176 +++++++++++++++++++++
 airflow/ui/src/utils/stateColor.ts                 |  37 -----
 23 files changed, 319 insertions(+), 106 deletions(-)

diff --git a/airflow/ui/src/components/DagRunInfo.tsx b/airflow/ui/src/components/DagRunInfo.tsx
index dd5569dd45a..dca0c3e2f3f 100644
--- a/airflow/ui/src/components/DagRunInfo.tsx
+++ b/airflow/ui/src/components/DagRunInfo.tsx
@@ -74,7 +74,7 @@ const DagRunInfo = ({
     >
       <Box>
         <Time datetime={dataIntervalStart} mr={2} showTooltip={false} />
-        {state === undefined ? undefined : <Status state={state}>{state}</Status>}
+        {state === undefined ? undefined : <Status state={state} />}
       </Box>
     </Tooltip>
   ) : undefined;
diff --git a/airflow/ui/src/components/MetricsBadge.tsx b/airflow/ui/src/components/MetricsBadge.tsx
index d744d2f6eb2..f763a9510b2 100644
--- a/airflow/ui/src/components/MetricsBadge.tsx
+++ b/airflow/ui/src/components/MetricsBadge.tsx
@@ -17,15 +17,16 @@
  * under the License.
  */
 import { Badge, type BadgeProps } from "@chakra-ui/react";
+import type { ReactNode } from "react";
 
 type MetricBadgeProps = {
-  readonly backgroundColor: string;
-  readonly color?: string;
+  readonly icon?: ReactNode;
   readonly runs?: number;
 } & BadgeProps;
 
-export const MetricsBadge = ({ backgroundColor, color = "fg.inverted", runs }: MetricBadgeProps) => (
-  <Badge bg={backgroundColor} borderRadius={15} color={color} minWidth={10} mr={1} px={4} py={1} size="md">
+export const MetricsBadge = ({ icon, runs, ...rest }: MetricBadgeProps) => (
+  <Badge {...rest} borderRadius={15} minWidth={10} mr={1} px={4} py={1} size="md" variant="solid">
+    {icon}
     {runs}
   </Badge>
 );
diff --git a/airflow/ui/src/components/StateIcon.tsx b/airflow/ui/src/components/StateIcon.tsx
new file mode 100644
index 00000000000..4102cae75df
--- /dev/null
+++ b/airflow/ui/src/components/StateIcon.tsx
@@ -0,0 +1,70 @@
+/*!
+ * 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 type { IconBaseProps } from "react-icons";
+import {
+  FiActivity,
+  FiAlertCircle,
+  FiAlertOctagon,
+  FiCalendar,
+  FiCheckCircle,
+  FiCircle,
+  FiRepeat,
+  FiSkipForward,
+  FiSlash,
+  FiWatch,
+} from "react-icons/fi";
+import { LuCalendarSync, LuRedo2 } from "react-icons/lu";
+import { PiQueue } from "react-icons/pi";
+
+import type { TaskInstanceState } from "openapi/requests/types.gen";
+
+type Props = {
+  readonly state?: TaskInstanceState | null;
+} & IconBaseProps;
+
+export const StateIcon = ({ state, ...rest }: Props) => {
+  switch (state) {
+    case "deferred":
+      return <FiWatch {...rest} />;
+    case "failed":
+      return <FiAlertOctagon {...rest} />;
+    case "queued":
+      return <PiQueue {...rest} />;
+    case "removed":
+      return <FiSlash {...rest} />;
+    case "restarting":
+      return <FiRepeat {...rest} />;
+    case "running":
+      return <FiActivity {...rest} />;
+    case "scheduled":
+      return <FiCalendar {...rest} />;
+    case "skipped":
+      return <FiSkipForward {...rest} />;
+    case "success":
+      return <FiCheckCircle {...rest} />;
+    case "up_for_reschedule":
+      return <LuCalendarSync {...rest} />;
+    case "up_for_retry":
+      return <LuRedo2 {...rest} />;
+    case "upstream_failed":
+      return <FiAlertCircle {...rest} />;
+    default:
+      return <FiCircle {...rest} />;
+  }
+};
diff --git a/airflow/ui/src/components/TaskInstanceTooltip.tsx b/airflow/ui/src/components/TaskInstanceTooltip.tsx
index bc44a23856f..ed09f9a9f93 100644
--- a/airflow/ui/src/components/TaskInstanceTooltip.tsx
+++ b/airflow/ui/src/components/TaskInstanceTooltip.tsx
@@ -38,6 +38,7 @@ const TaskInstanceTooltip = ({ children, positioning, taskInstance, ...rest }: P
       {...rest}
       content={
         <Box>
+          <Text>State: {taskInstance.state}</Text>
           {"dag_run_id" in taskInstance ? <Text>Run ID: {taskInstance.dag_run_id}</Text> : undefined}
           <Text>
             Start Date: <Time datetime={taskInstance.start_date} />
diff --git a/airflow/ui/src/components/ui/Status.tsx b/airflow/ui/src/components/ui/Status.tsx
index 91ef7e15309..bd887ba7c59 100644
--- a/airflow/ui/src/components/ui/Status.tsx
+++ b/airflow/ui/src/components/ui/Status.tsx
@@ -16,26 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Status as ChakraStatus } from "@chakra-ui/react";
+import { Status as ChakraStatus, chakra } from "@chakra-ui/react";
 import * as React from "react";
 
 import type { DagRunState, TaskInstanceState } from "openapi/requests/types.gen";
-import { stateColor } from "src/utils/stateColor";
+
+import { StateIcon } from "../StateIcon";
 
 type StatusValue = DagRunState | TaskInstanceState | null;
 
 export type StatusProps = {
-  state: StatusValue;
+  state?: StatusValue;
 } & ChakraStatus.RootProps;
 
-export const Status = React.forwardRef<HTMLDivElement, StatusProps>(({ children, state, ...rest }, ref) => {
-  // "null" is actually a string on stateColor
-  const colorPalette = stateColor[state ?? "null"];
-
-  return (
-    <ChakraStatus.Root ref={ref} {...rest}>
-      <ChakraStatus.Indicator bg={colorPalette} />
-      {children}
-    </ChakraStatus.Root>
-  );
-});
+export const Status = React.forwardRef<HTMLDivElement, StatusProps>(({ children, state, ...rest }, ref) => (
+  <ChakraStatus.Root ref={ref} {...rest}>
+    <chakra.span color={`${state}.solid`}>
+      <StateIcon state={state} />
+    </chakra.span>
+    {children}
+  </ChakraStatus.Root>
+));
diff --git a/airflow/ui/src/layouts/Details/Graph/Graph.tsx b/airflow/ui/src/layouts/Details/Graph/Graph.tsx
index f6972255f61..ee2c270dd6e 100644
--- a/airflow/ui/src/layouts/Details/Graph/Graph.tsx
+++ b/airflow/ui/src/layouts/Details/Graph/Graph.tsx
@@ -24,7 +24,6 @@ import { useParams } from "react-router-dom";
 import { useGridServiceGridData, useStructureServiceStructureData } from "openapi/queries";
 import { useColorMode } from "src/context/colorMode";
 import { useOpenGroups } from "src/context/openGroups";
-import { stateColor } from "src/utils/stateColor";
 
 import Edge from "./Edge";
 import { JoinNode } from "./JoinNode";
@@ -42,7 +41,7 @@ const nodeColor = (
   }
 
   if (taskInstance?.state !== undefined && !isOpen) {
-    return stateColor[taskInstance.state ?? "null"];
+    return `var(--chakra-colors-${taskInstance.state}-solid)`;
   }
 
   if (isOpen && depth !== undefined && depth % 2 === 0) {
diff --git a/airflow/ui/src/layouts/Details/Graph/TaskNode.tsx b/airflow/ui/src/layouts/Details/Graph/TaskNode.tsx
index 1372e824a0c..f6ba9b69bad 100644
--- a/airflow/ui/src/layouts/Details/Graph/TaskNode.tsx
+++ b/airflow/ui/src/layouts/Details/Graph/TaskNode.tsx
@@ -24,7 +24,6 @@ import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
 import { Status } from "src/components/ui";
 import { useOpenGroups } from "src/context/openGroups";
 import { pluralize } from "src/utils";
-import { stateColor } from "src/utils/stateColor";
 
 import { NodeWrapper } from "./NodeWrapper";
 import { TaskName } from "./TaskName";
@@ -67,9 +66,7 @@ export const TaskNode = ({
           <Flex
             // Alternate background color for nested open groups
             bg={isOpen && depth !== undefined && depth % 2 === 0 ? "bg.muted" : "bg"}
-            borderColor={
-              taskInstance?.state === undefined ? "border" : stateColor[taskInstance.state ?? "null"]
-            }
+            borderColor={taskInstance?.state ? `${taskInstance.state}.solid` : "border"}
             borderRadius={5}
             borderWidth={isSelected ? 6 : 2}
             height={`${height}px`}
@@ -87,12 +84,12 @@ export const TaskNode = ({
                 label={label}
                 setupTeardownType={setupTeardownType}
               />
-              <Text color="fg.muted" fontSize="xs" mb={-1} mt={2} textTransform="capitalize">
+              <Text color="fg.muted" fontSize="sm" mt={1} textTransform="capitalize">
                 {isGroup ? "Task Group" : operator}
               </Text>
               {taskInstance === undefined ? undefined : (
                 <HStack>
-                  <Status fontSize="xs" state={taskInstance.state}>
+                  <Status fontSize="sm" state={taskInstance.state}>
                     {taskInstance.state}
                   </Status>
                   {taskInstance.try_number > 1 ? <MdRefresh /> : undefined}
diff --git a/airflow/ui/src/main.tsx b/airflow/ui/src/main.tsx
index a89f0f11cbb..a5f6a35cfc1 100644
--- a/airflow/ui/src/main.tsx
+++ b/airflow/ui/src/main.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
+import { ChakraProvider } from "@chakra-ui/react";
 import { QueryClientProvider } from "@tanstack/react-query";
 import axios, { type AxiosError } from "axios";
 import { StrictMode } from "react";
@@ -28,6 +28,7 @@ import { TimezoneProvider } from "src/context/timezone";
 import { router } from "src/router";
 
 import { queryClient } from "./queryClient";
+import { system } from "./theme";
 
 // redirect to login page if the API responds with unauthorized or forbidden errors
 axios.interceptors.response.use(
@@ -46,7 +47,7 @@ axios.interceptors.response.use(
 
 createRoot(document.querySelector("#root") as HTMLDivElement).render(
   <StrictMode>
-    <ChakraProvider value={defaultSystem}>
+    <ChakraProvider value={system}>
       <ColorModeProvider>
         <QueryClientProvider client={queryClient}>
           <TimezoneProvider>
diff --git a/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow/ui/src/pages/Dag/Overview/Overview.tsx
index a79ebfb4019..d89db680f74 100644
--- a/airflow/ui/src/pages/Dag/Overview/Overview.tsx
+++ b/airflow/ui/src/pages/Dag/Overview/Overview.tsx
@@ -24,7 +24,6 @@ import { useParams } from "react-router-dom";
 import { useDagRunServiceGetDagRuns, useTaskInstanceServiceGetTaskInstances } from "openapi/queries";
 import TimeRangeSelector from "src/components/TimeRangeSelector";
 import { TrendCountButton } from "src/components/TrendCountButton";
-import { stateColor } from "src/utils/stateColor";
 
 const defaultHour = "168";
 
@@ -64,7 +63,7 @@ export const Overview = () => {
       </Box>
       <HStack>
         <TrendCountButton
-          colorPalette={stateColor.failed}
+          colorPalette="failed"
           count={failedTasks?.total_entries ?? 0}
           endDate={endDate}
           events={(failedTasks?.task_instances ?? []).map((ti) => ({
@@ -79,7 +78,7 @@ export const Overview = () => {
           startDate={startDate}
         />
         <TrendCountButton
-          colorPalette={stateColor.failed}
+          colorPalette="failed"
           count={failedRuns?.total_entries ?? 0}
           endDate={endDate}
           events={(failedRuns?.dag_runs ?? []).map((dr) => ({
diff --git a/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx b/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
index 2d517dd4e57..db8c01c1d77 100644
--- a/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
+++ b/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
@@ -63,9 +63,7 @@ export const TaskCard = ({ dagId, task, taskInstances }: Props) => (
             <Link asChild color="fg.info" fontSize="sm">
               <RouterLink to={getTaskInstanceLink(taskInstances[0])}>
                 <Time datetime={taskInstances[0].start_date} />
-                {taskInstances[0].state === null ? undefined : (
-                  <Status state={taskInstances[0].state}>{taskInstances[0].state}</Status>
-                )}
+                {taskInstances[0].state === null ? undefined : <Status state={taskInstances[0].state} />}
               </RouterLink>
             </Link>
           </TaskInstanceTooltip>
diff --git a/airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx b/airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx
index fee2b943582..3a7fc13d449 100644
--- a/airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx
+++ b/airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx
@@ -24,7 +24,6 @@ import { Link } from "react-router-dom";
 import type { TaskInstanceResponse } from "openapi/requests/types.gen";
 import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
 import { getTaskInstanceLink } from "src/utils/links";
-import { stateColor } from "src/utils/stateColor";
 
 dayjs.extend(duration);
 
@@ -58,7 +57,7 @@ export const TaskRecentRuns = ({
             <Link to={getTaskInstanceLink(taskInstance)}>
               <Box p={1}>
                 <Box
-                  bg={stateColor[taskInstance.state]}
+                  bg={`${taskInstance.state}.solid`}
                   borderRadius="4px"
                   height={`${(taskInstance.duration / max) * BAR_HEIGHT}px`}
                   minHeight={1}
diff --git a/airflow/ui/src/pages/DagsList/RecentRuns.tsx b/airflow/ui/src/pages/DagsList/RecentRuns.tsx
index 99f33c9bd02..4b803551230 100644
--- a/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -24,7 +24,6 @@ import { Link } from "react-router-dom";
 import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
 import Time from "src/components/Time";
 import { Tooltip } from "src/components/ui";
-import { stateColor } from "src/utils/stateColor";
 
 dayjs.extend(duration);
 
@@ -74,7 +73,7 @@ export const RecentRuns = ({
           <Link to={`/dags/${run.dag_id}/runs/${run.dag_run_id}/`}>
             <Box px={1}>
               <Box
-                bg={stateColor[run.state]}
+                bg={`${run.state}.solid`}
                 borderRadius="4px"
                 height={`${(run.duration / max) * BAR_HEIGHT}px`}
                 minHeight={1}
diff --git a/airflow/ui/src/pages/Dashboard/Health/HealthTag.tsx b/airflow/ui/src/pages/Dashboard/Health/HealthTag.tsx
index 31920db3133..35b5f75b6c2 100644
--- a/airflow/ui/src/pages/Dashboard/Health/HealthTag.tsx
+++ b/airflow/ui/src/pages/Dashboard/Health/HealthTag.tsx
@@ -16,8 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Skeleton, TagLabel, Text } from "@chakra-ui/react";
+import { Skeleton, TagLabel, Text, HStack } from "@chakra-ui/react";
 
+import { StateIcon } from "src/components/StateIcon";
 import Time from "src/components/Time";
 import { Tag, Tooltip } from "src/components/ui";
 import { capitalize } from "src/utils";
@@ -37,6 +38,8 @@ export const HealthTag = ({
     return <Skeleton borderRadius="full" height={8} width={24} />;
   }
 
+  const state = status === "healthy" ? "success" : "failed";
+
   return (
     <Tooltip
       content={
@@ -49,8 +52,11 @@ export const HealthTag = ({
       }
       disabled={!Boolean(latestHeartbeat)}
     >
-      <Tag borderRadius="full" colorPalette={status === "healthy" ? "green" : "red"} size="lg">
-        <TagLabel>{title}</TagLabel>
+      <Tag borderRadius="full" colorPalette={state} size="lg">
+        <HStack>
+          <StateIcon state={state} />
+          <TagLabel>{title}</TagLabel>
+        </HStack>
       </Tag>
     </Tooltip>
   );
diff --git a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvents.tsx b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvents.tsx
index 21c425f0c5a..2021a6add8f 100644
--- a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvents.tsx
+++ b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvents.tsx
@@ -51,7 +51,7 @@ export const AssetEvents = ({ assetSortBy, endDate, setAssetSortBy, startDate }:
     <Box borderRadius={5} borderWidth={1} ml={2} pb={2}>
       <Flex justify="space-between" mr={1} mt={0} pl={3} pt={1}>
         <HStack>
-          <MetricsBadge backgroundColor="blue.solid" runs={isLoading ? 0 : data?.total_entries} />
+          <MetricsBadge colorPalette="blue" runs={isLoading ? 0 : data?.total_entries} />
           <Heading marginEnd="auto" size="md">
             Asset Events
           </Heading>
diff --git a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx
index 75b18c85fe6..c298ee92aa2 100644
--- a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx
+++ b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/DagRunMetrics.tsx
@@ -33,7 +33,7 @@ const DAGRUN_STATES: Array<keyof DAGRunStates> = ["queued", "running", "success"
 export const DagRunMetrics = ({ dagRunStates, total }: DagRunMetricsProps) => (
   <Box borderRadius={5} borderWidth={1} p={2}>
     <HStack mb={4}>
-      <MetricsBadge backgroundColor="blue.solid" runs={total} />
+      <MetricsBadge colorPalette="blue" runs={total} />
       <Heading size="md">Dag Runs</Heading>
     </HStack>
     {DAGRUN_STATES.map((state) => (
diff --git a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
index c2455a8c961..736943afd67 100644
--- a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
+++ b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
@@ -20,7 +20,6 @@ import { Box, Flex, HStack, VStack, Text } from "@chakra-ui/react";
 
 import { MetricsBadge } from "src/components/MetricsBadge";
 import { capitalize } from "src/utils";
-import { stateColor } from "src/utils/stateColor";
 
 const BAR_WIDTH = 100;
 const BAR_HEIGHT = 5;
@@ -42,14 +41,14 @@ export const MetricSection = ({ runs, state, total }: MetricSectionProps) => {
     <VStack align="left" gap={1} mb={4} ml={0} pl={0}>
       <Flex justify="space-between">
         <HStack>
-          <MetricsBadge backgroundColor={stateColor[state as keyof typeof stateColor]} runs={runs} />
+          <MetricsBadge colorPalette={state} runs={runs} />
           <Text> {capitalize(state)} </Text>
         </HStack>
         <Text color="fg.muted"> {statePercent}% </Text>
       </Flex>
       <HStack gap={0} mt={2}>
         <Box
-          bg={stateColor[state as keyof typeof stateColor]}
+          bg={`${state}.solid`}
           borderLeftRadius={5}
           height={`${BAR_HEIGHT}px`}
           minHeight={2}
diff --git a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx
index 1e92f42dfd3..143b0f2c737 100644
--- a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx
+++ b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/TaskInstanceMetrics.tsx
@@ -39,7 +39,7 @@ const TASK_STATES: Array<keyof TaskInstanceStateCount> = [
 export const TaskInstanceMetrics = ({ taskInstanceStates, total }: TaskInstanceMetricsProps) => (
   <Box borderRadius={5} borderWidth={1} mt={2} p={2}>
     <HStack mb={4}>
-      <MetricsBadge backgroundColor="blue.solid" runs={total} />
+      <MetricsBadge colorPalette="blue" runs={total} />
       <Heading size="md">Task Instances</Heading>
     </HStack>
 
diff --git a/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx b/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx
index e28efa0f9e0..9b6723ae849 100644
--- a/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx
+++ b/airflow/ui/src/pages/Dashboard/Stats/DAGImportErrors.tsx
@@ -22,7 +22,6 @@ import { FiChevronRight } from "react-icons/fi";
 import { useImportErrorServiceGetImportErrors } from "openapi/queries";
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { MetricsBadge } from "src/components/MetricsBadge";
-import { stateColor } from "src/utils/stateColor";
 
 import { DAGImportErrorsModal } from "./DAGImportErrorsModal";
 
@@ -49,7 +48,7 @@ export const DAGImportErrors = () => {
           onClick={onOpen}
           variant="outline"
         >
-          <MetricsBadge backgroundColor={stateColor.failed} runs={importErrorsCount} />
+          <MetricsBadge colorPalette="failed" runs={importErrorsCount} />
           <Box alignItems="center" display="flex" gap={1}>
             <Text fontWeight="bold">Dag Import Errors</Text>
             <FiChevronRight />
diff --git a/airflow/ui/src/pages/Dashboard/Stats/DagFilterButton.tsx b/airflow/ui/src/pages/Dashboard/Stats/DagFilterButton.tsx
index b9498566fe8..f5cdaf49f60 100644
--- a/airflow/ui/src/pages/Dashboard/Stats/DagFilterButton.tsx
+++ b/airflow/ui/src/pages/Dashboard/Stats/DagFilterButton.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Box, Text, Button } from "@chakra-ui/react";
+import type { ReactNode } from "react";
 import { FiChevronRight } from "react-icons/fi";
 import { Link as RouterLink } from "react-router-dom";
 
@@ -26,18 +27,20 @@ import { capitalize } from "src/utils";
 // TODO: Add badge count once API is available
 
 export const DagFilterButton = ({
-  badgeColor,
+  colorPalette,
   filter,
+  icon,
   link,
 }: {
-  readonly badgeColor: string;
+  readonly colorPalette: string;
   readonly filter: string;
+  readonly icon?: ReactNode;
   readonly link: string;
 }) => (
   <RouterLink to={link}>
     <Button alignItems="center" borderRadius="md" display="flex" gap={2} variant="outline">
       <Box alignItems="center" display="flex" gap={1}>
-        <MetricsBadge backgroundColor={badgeColor} />
+        <MetricsBadge colorPalette={colorPalette} icon={icon} />
         <Text fontWeight="bold">{capitalize(filter)} Dags</Text>
         <FiChevronRight />
       </Box>
diff --git a/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx b/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
index b71b54d4245..87371c40eda 100644
--- a/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
+++ b/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
@@ -19,7 +19,7 @@
 import { Box, Flex, Heading, HStack } from "@chakra-ui/react";
 import { FiClipboard } from "react-icons/fi";
 
-import { stateColor } from "src/utils/stateColor";
+import { StateIcon } from "src/components/StateIcon";
 
 import { DAGImportErrors } from "./DAGImportErrors";
 import { DagFilterButton } from "./DagFilterButton";
@@ -33,14 +33,20 @@ export const Stats = () => (
       </Heading>
     </Flex>
     <HStack>
-      <DagFilterButton badgeColor={stateColor.failed} filter="failed" link="dags?last_dag_run_state=failed" />
+      <DagFilterButton
+        colorPalette="failed"
+        filter="failed"
+        icon={<StateIcon state="failed" />}
+        link="dags?last_dag_run_state=failed"
+      />
       <DAGImportErrors />
       <DagFilterButton
-        badgeColor={stateColor.running}
+        colorPalette="running"
         filter="running"
+        icon={<StateIcon state="running" />}
         link="dags?last_dag_run_state=running"
       />
-      <DagFilterButton badgeColor="blue.solid" filter="active" link="dags?paused=false" />
+      <DagFilterButton colorPalette="blue" filter="active" link="dags?paused=false" />
     </HStack>
   </Box>
 );
diff --git a/airflow/ui/src/pages/Pools/PoolBar.tsx b/airflow/ui/src/pages/Pools/PoolBar.tsx
index 1fead035016..0cc8a1a1c47 100644
--- a/airflow/ui/src/pages/Pools/PoolBar.tsx
+++ b/airflow/ui/src/pages/Pools/PoolBar.tsx
@@ -19,21 +19,20 @@
  * under the License.
  */
 import { Box, Flex, HStack, Text, VStack } from "@chakra-ui/react";
-import React from "react";
-import { FiActivity, FiCalendar, FiCheckCircle, FiClock, FiList, FiXCircle } from "react-icons/fi";
+import { FiXCircle } from "react-icons/fi";
 
 import type { PoolResponse } from "openapi/requests/types.gen";
+import { StateIcon } from "src/components/StateIcon";
 import { Tooltip } from "src/components/ui";
 import { capitalize } from "src/utils";
-import { stateColor } from "src/utils/stateColor";
 
 const slots = {
-  open_slots: { color: stateColor.success, icon: FiCheckCircle },
-  occupied_slots: { color: stateColor.up_for_retry, icon: FiXCircle },
-  running_slots: { color: stateColor.running, icon: FiActivity },
-  queued_slots: { color: stateColor.queued, icon: FiList },
-  scheduled_slots: { color: stateColor.scheduled, icon: FiCalendar },
-  deferred_slots: { color: stateColor.deferred, icon: FiClock },
+  open_slots: { color: "success", icon: <StateIcon state="success" /> },
+  occupied_slots: { color: "up_for_retry", icon: <FiXCircle /> },
+  running_slots: { color: "running", icon: <StateIcon state="running" /> },
+  queued_slots: { color: "queued", icon: <StateIcon state="queued" /> },
+  scheduled_slots: { color: "scheduled", icon: <StateIcon state="scheduled" /> },
+  deferred_slots: { color: "deferred", icon: <StateIcon state="deferred" /> },
 };
 
 type PoolBarProps = {
@@ -50,7 +49,7 @@ const PoolBar = ({ pool }: PoolBarProps) => (
           </Text>
           {pool.include_deferred ? (
             <Tooltip content="Deferred Slots Included">
-              <FiClock size={18} />
+              <StateIcon size={18} state="deferred" />
             </Tooltip>
           ) : undefined}
         </HStack>
@@ -77,13 +76,13 @@ const PoolBar = ({ pool }: PoolBarProps) => (
             <Tooltip content={`${capitalize(slotKey.replace("_", " "))}: ${slotValue}`} key={slotKey}>
               <Flex
                 alignItems="center"
-                bg={color}
+                colorPalette={color}
                 flex={flexValue}
                 h="100%"
                 justifyContent="center"
                 position="relative"
               >
-                {React.createElement(icon, { size: 16, color: "white" })}
+                {icon}
               </Flex>
             </Tooltip>
           );
diff --git a/airflow/ui/src/theme.ts b/airflow/ui/src/theme.ts
new file mode 100644
index 00000000000..706ae0ed57e
--- /dev/null
+++ b/airflow/ui/src/theme.ts
@@ -0,0 +1,176 @@
+/*!
+ * 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.
+ */
+
+/* eslint-disable perfectionist/sort-objects */
+import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react";
+
+const generateSemanticTokens = (color: string, darkContrast: string = "white") => ({
+  solid: { value: `{colors.${color}.600}` },
+  contrast: { value: { _light: "white", _dark: darkContrast } },
+  fg: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } },
+  muted: { value: { _light: `{colors.${color}.200}`, _dark: `{colors.${color}.800}` } },
+  subtle: { value: { _light: `{colors.${color}.100}`, _dark: `{colors.${color}.900}` } },
+  emphasized: { value: { _light: `{colors.${color}.300}`, _dark: `{colors.${color}.700}` } },
+  focusRing: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } },
+});
+
+const customConfig = defineConfig({
+  theme: {
+    tokens: {
+      colors: {
+        // Default green was too light
+        success: {
+          "50": { value: "#E0FFE0" },
+          "100": { value: "#C2FFC2" },
+          "200": { value: "#80FF80" },
+          "300": { value: "#42FF42" },
+          "400": { value: "#00FF00" },
+          "500": { value: "#00C200" },
+          "600": { value: "#008000" },
+          "700": { value: "#006100" },
+          "800": { value: "#004200" },
+          "900": { value: "#001F00" },
+          "950": { value: "#000F00" },
+        },
+        failed: defaultConfig.theme?.tokens?.colors?.red ?? {},
+        // Default gray was too dark
+        queued: {
+          "50": { value: "#F5F5F5" },
+          "100": { value: "#EBEBEB" },
+          "200": { value: "#D4D4D4" },
+          "300": { value: "#BFBFBF" },
+          "400": { value: "#ABABAB" },
+          "500": { value: "#969696" },
+          "600": { value: "#808080" },
+          "700": { value: "#616161" },
+          "800": { value: "#404040" },
+          "900": { value: "#212121" },
+          "950": { value: "#0F0F0F" },
+        },
+        skipped: defaultConfig.theme?.tokens?.colors?.pink ?? {},
+        up_for_reschedule: defaultConfig.theme?.tokens?.colors?.cyan ?? {},
+        up_for_retry: defaultConfig.theme?.tokens?.colors?.yellow ?? {},
+        upstream_failed: defaultConfig.theme?.tokens?.colors?.orange ?? {},
+        // lime
+        running: {
+          "50": { value: "#EFFBEF" },
+          "100": { value: "#DEF7DE" },
+          "200": { value: "#B9EEB9" },
+          "300": { value: "#98E698" },
+          "400": { value: "#78DE78" },
+          "500": { value: "#53D553" },
+          "600": { value: "#32CD32" },
+          "700": { value: "#269C26" },
+          "800": { value: "#196719" },
+          "900": { value: "#0D350D" },
+          "950": { value: "#061906" },
+        },
+        // violet
+        restarting: {
+          "50": { value: "#F6EBFF" },
+          "100": { value: "#EDD6FF" },
+          "200": { value: "#D9A8FF" },
+          "300": { value: "#C880FF" },
+          "400": { value: "#B657FF" },
+          "500": { value: "#A229FF" },
+          "600": { value: "#8F00FF" },
+          "700": { value: "#6E00C2" },
+          "800": { value: "#480080" },
+          "900": { value: "#260042" },
+          "950": { value: "#11001F" },
+        },
+        // mediumpurple
+        deferred: {
+          "50": { value: "#F6F3FC" },
+          "100": { value: "#EDE7F9" },
+          "200": { value: "#DACEF3" },
+          "300": { value: "#C8B6ED" },
+          "400": { value: "#B9A1E7" },
+          "500": { value: "#A689E1" },
+          "600": { value: "#9370DB" },
+          "700": { value: "#6432C8" },
+          "800": { value: "#412182" },
+          "900": { value: "#211041" },
+          "950": { value: "#100821" },
+        },
+        // tan
+        scheduled: {
+          "50": { value: "#FBF8F4" },
+          "100": { value: "#F8F3ED" },
+          "200": { value: "#F1E7DA" },
+          "300": { value: "#E8D9C4" },
+          "400": { value: "#E1CDB2" },
+          "500": { value: "#DAC1A0" },
+          "600": { value: "#D2B48C" },
+          "700": { value: "#B9894B" },
+          "800": { value: "#7D5C31" },
+          "900": { value: "#3E2E18" },
+          "950": { value: "#21180D" },
+        },
+        // lightblue
+        none: {
+          "50": { value: "#F7FBFD" },
+          "100": { value: "#F3F9FB" },
+          "200": { value: "#E4F2F7" },
+          "300": { value: "#D8ECF3" },
+          "400": { value: "#C8E5EE" },
+          "500": { value: "#BDDFEB" },
+          "600": { value: "#ADD8E6" },
+          "700": { value: "#5FB2CE" },
+          "800": { value: "#30819C" },
+          "900": { value: "#18414E" },
+          "950": { value: "#0C2027" },
+        },
+        // lightgrey
+        removed: {
+          "50": { value: "#FCFCFC" },
+          "100": { value: "#F7F7F7" },
+          "200": { value: "#F0F0F0" },
+          "300": { value: "#E8E8E8" },
+          "400": { value: "#E0E0E0" },
+          "500": { value: "#DBDBDB" },
+          "600": { value: "#D3D3D3" },
+          "700": { value: "#9E9E9E" },
+          "800": { value: "#696969" },
+          "900": { value: "#363636" },
+          "950": { value: "#1A1A1A" },
+        },
+      },
+    },
+    semanticTokens: {
+      colors: {
+        success: generateSemanticTokens("success"),
+        failed: defaultConfig.theme?.semanticTokens?.colors?.red ?? {},
+        queued: generateSemanticTokens("queued"),
+        skipped: defaultConfig.theme?.semanticTokens?.colors?.pink ?? {},
+        up_for_reschedule: defaultConfig.theme?.semanticTokens?.colors?.cyan ?? {},
+        up_for_retry: defaultConfig.theme?.semanticTokens?.colors?.yellow ?? {},
+        upstream_failed: defaultConfig.theme?.semanticTokens?.colors?.orange ?? {},
+        running: generateSemanticTokens("running"),
+        restarting: generateSemanticTokens("restarting"),
+        deferred: generateSemanticTokens("deferred"),
+        scheduled: generateSemanticTokens("scheduled"),
+        none: generateSemanticTokens("none", "black"),
+        removed: generateSemanticTokens("removed", "black"),
+      },
+    },
+  },
+});
+
+export const system = createSystem(defaultConfig, customConfig);
diff --git a/airflow/ui/src/utils/stateColor.ts b/airflow/ui/src/utils/stateColor.ts
deleted file mode 100644
index 555cd0fbdfd..00000000000
--- a/airflow/ui/src/utils/stateColor.ts
+++ /dev/null
@@ -1,37 +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.
- */
-
-// TODO: replace this with Airflow config values
-
-export const stateColor = {
-  deferred: "mediumpurple",
-  failed: "red",
-  none: "lightblue",
-  null: "lightblue",
-  queued: "gray",
-  removed: "lightgrey",
-  restarting: "violet",
-  running: "lime",
-  scheduled: "tan",
-  skipped: "hotpink",
-  success: "green",
-  up_for_reschedule: "turquoise",
-  up_for_retry: "gold",
-  upstream_failed: "orange",
-};