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 15f09c736a Add timezone selection to new UI (#43132)
15f09c736a is described below

commit 15f09c736a17bc406f0e97106113ebcac7b0a3a1
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu Oct 17 23:00:03 2024 +0100

    Add timezone selection to new UI (#43132)
---
 airflow/ui/package.json                            |  1 +
 airflow/ui/pnpm-lock.yaml                          |  8 ++
 airflow/ui/src/components/Time.test.tsx            | 64 ++++++++++++++++
 airflow/ui/src/components/Time.tsx                 | 61 +++++++++++++++
 .../ui/src/context/timezone/TimezoneProvider.tsx   | 59 ++++++++++++++
 .../Wrapper.tsx => context/timezone/index.ts}      | 23 +-----
 .../timezone/useTimezone.ts}                       | 29 +++----
 .../Wrapper.tsx => layouts/Nav/TimezoneModal.tsx}  | 47 +++++++-----
 airflow/ui/src/layouts/Nav/TimezoneSelector.tsx    | 89 ++++++++++++++++++++++
 airflow/ui/src/layouts/Nav/UserSettingsButton.tsx  | 19 ++++-
 airflow/ui/src/main.tsx                            |  5 +-
 airflow/ui/src/pages/DagsList/DagCard.tsx          |  5 +-
 airflow/ui/src/pages/DagsList/DagsList.tsx         |  5 ++
 airflow/ui/src/pages/Dashboard/HealthTag.tsx       |  5 +-
 airflow/ui/src/utils/Wrapper.tsx                   |  6 +-
 airflow/ui/tsconfig.app.json                       |  4 +-
 16 files changed, 364 insertions(+), 66 deletions(-)

diff --git a/airflow/ui/package.json b/airflow/ui/package.json
index c54c9af5bb..3ca8d1a06f 100644
--- a/airflow/ui/package.json
+++ b/airflow/ui/package.json
@@ -24,6 +24,7 @@
     "@tanstack/react-table": "^8.20.1",
     "axios": "^1.7.7",
     "chakra-react-select": "^4.9.2",
+    "dayjs": "^1.11.13",
     "framer-motion": "^11.3.29",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
diff --git a/airflow/ui/pnpm-lock.yaml b/airflow/ui/pnpm-lock.yaml
index 3b73df0fa8..3ceee513bb 100644
--- a/airflow/ui/pnpm-lock.yaml
+++ b/airflow/ui/pnpm-lock.yaml
@@ -32,6 +32,9 @@ importers:
       chakra-react-select:
         specifier: ^4.9.2
         version: 4.9.2(uzcvocchpeesoxvtkif6ppnvaq)
+      dayjs:
+        specifier: ^1.11.13
+        version: 1.11.13
       framer-motion:
         specifier: ^11.3.29
         version: 
11.3.29(@emotion/[email protected])([email protected]([email protected]))([email protected])
@@ -1725,6 +1728,9 @@ packages:
     resolution: {integrity: 
sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
     engines: {node: '>= 0.4'}
 
+  [email protected]:
+    resolution: {integrity: 
sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
+
   [email protected]:
     resolution: {integrity: 
sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==}
     engines: {node: '>=6.0'}
@@ -5242,6 +5248,8 @@ snapshots:
       es-errors: 1.3.0
       is-data-view: 1.0.1
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       ms: 2.1.2
diff --git a/airflow/ui/src/components/Time.test.tsx 
b/airflow/ui/src/components/Time.test.tsx
new file mode 100644
index 0000000000..9e59d96dd4
--- /dev/null
+++ b/airflow/ui/src/components/Time.test.tsx
@@ -0,0 +1,64 @@
+/*!
+ * 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 { render, screen } from "@testing-library/react";
+import dayjs from "dayjs";
+import { describe, it, expect, vi } from "vitest";
+
+import { TimezoneContext } from "src/context/timezone";
+import { Wrapper } from "src/utils/Wrapper";
+
+import Time, { defaultFormat, defaultFormatWithTZ } from "./Time";
+
+describe("Test Time and TimezoneProvider", () => {
+  it("Displays a UTC time correctly", () => {
+    const now = new Date();
+
+    render(<Time datetime={now.toISOString()} />, {
+      wrapper: Wrapper,
+    });
+
+    const utcTime = screen.getByText(dayjs.utc(now).format(defaultFormat));
+
+    expect(utcTime).toBeDefined();
+    expect(utcTime.title).toBeFalsy();
+  });
+
+  it("Displays a set timezone, includes UTC date in title", () => {
+    const now = new Date();
+    const tz = "US/Samoa";
+
+    render(
+      <TimezoneContext.Provider
+        value={{ selectedTimezone: tz, setSelectedTimezone: vi.fn() }}
+      >
+        <Time datetime={now.toISOString()} />
+      </TimezoneContext.Provider>,
+      {
+        wrapper: Wrapper,
+      },
+    );
+
+    const samoaTime = 
screen.getByText(dayjs(now).tz(tz).format(defaultFormat));
+
+    expect(samoaTime).toBeDefined();
+    expect(samoaTime.title).toEqual(
+      dayjs().tz("UTC").format(defaultFormatWithTZ),
+    );
+  });
+});
diff --git a/airflow/ui/src/components/Time.tsx 
b/airflow/ui/src/components/Time.tsx
new file mode 100644
index 0000000000..d7fb97e94c
--- /dev/null
+++ b/airflow/ui/src/components/Time.tsx
@@ -0,0 +1,61 @@
+/*!
+ * 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 dayjs from "dayjs";
+import advancedFormat from "dayjs/plugin/advancedFormat";
+import tz from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+
+import { useTimezone } from "src/context/timezone";
+
+export const defaultFormat = "YYYY-MM-DD, HH:mm:ss";
+export const defaultFormatWithTZ = `${defaultFormat} z`;
+export const defaultTZFormat = "z (Z)";
+
+dayjs.extend(utc);
+dayjs.extend(tz);
+dayjs.extend(advancedFormat);
+
+type Props = {
+  readonly datetime?: string | null;
+  readonly format?: string;
+};
+
+const Time = ({ datetime, format = defaultFormat }: Props) => {
+  const { selectedTimezone } = useTimezone();
+  const time = dayjs(datetime);
+
+  if (datetime === null || datetime === undefined || !time.isValid()) {
+    return undefined;
+  }
+
+  const formattedTime = time.tz(selectedTimezone).format(format);
+  const utcTime = time.tz("UTC").format(defaultFormatWithTZ);
+
+  return (
+    <time
+      dateTime={datetime}
+      // show title if date is not UTC
+      title={selectedTimezone.toUpperCase() === "UTC" ? undefined : utcTime}
+    >
+      {formattedTime}
+    </time>
+  );
+};
+
+export default Time;
diff --git a/airflow/ui/src/context/timezone/TimezoneProvider.tsx 
b/airflow/ui/src/context/timezone/TimezoneProvider.tsx
new file mode 100644
index 0000000000..dfe40f6976
--- /dev/null
+++ b/airflow/ui/src/context/timezone/TimezoneProvider.tsx
@@ -0,0 +1,59 @@
+/*!
+ * 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 {
+  createContext,
+  useState,
+  useMemo,
+  type PropsWithChildren,
+} from "react";
+
+export type TimezoneContextType = {
+  selectedTimezone: string;
+  setSelectedTimezone: (timezone: string) => void;
+};
+
+export const TimezoneContext = createContext<TimezoneContextType | undefined>(
+  undefined,
+);
+
+const TIMEZONE_KEY = "timezone";
+
+export const TimezoneProvider = ({ children }: PropsWithChildren) => {
+  const [selectedTimezone, setSelectedTimezone] = useState(() => {
+    const timezone = localStorage.getItem(TIMEZONE_KEY);
+
+    return timezone ?? "UTC";
+  });
+
+  const selectTimezone = (tz: string) => {
+    localStorage.setItem(TIMEZONE_KEY, tz);
+    setSelectedTimezone(tz);
+  };
+
+  const value = useMemo<TimezoneContextType>(
+    () => ({ selectedTimezone, setSelectedTimezone: selectTimezone }),
+    [selectedTimezone],
+  );
+
+  return (
+    <TimezoneContext.Provider value={value}>
+      {children}
+    </TimezoneContext.Provider>
+  );
+};
diff --git a/airflow/ui/src/utils/Wrapper.tsx 
b/airflow/ui/src/context/timezone/index.ts
similarity index 57%
copy from airflow/ui/src/utils/Wrapper.tsx
copy to airflow/ui/src/context/timezone/index.ts
index 5dce11b333..a60c7c04b8 100644
--- a/airflow/ui/src/utils/Wrapper.tsx
+++ b/airflow/ui/src/context/timezone/index.ts
@@ -16,25 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ChakraProvider } from "@chakra-ui/react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import type { PropsWithChildren } from "react";
-import { MemoryRouter } from "react-router-dom";
 
-export const Wrapper = ({ children }: PropsWithChildren) => {
-  const queryClient = new QueryClient({
-    defaultOptions: {
-      queries: {
-        staleTime: Infinity,
-      },
-    },
-  });
-
-  return (
-    <ChakraProvider>
-      <QueryClientProvider client={queryClient}>
-        <MemoryRouter>{children}</MemoryRouter>
-      </QueryClientProvider>
-    </ChakraProvider>
-  );
-};
+export * from "./TimezoneProvider";
+export * from "./useTimezone";
diff --git a/airflow/ui/src/utils/Wrapper.tsx 
b/airflow/ui/src/context/timezone/useTimezone.ts
similarity index 57%
copy from airflow/ui/src/utils/Wrapper.tsx
copy to airflow/ui/src/context/timezone/useTimezone.ts
index 5dce11b333..a96b487fe7 100644
--- a/airflow/ui/src/utils/Wrapper.tsx
+++ b/airflow/ui/src/context/timezone/useTimezone.ts
@@ -16,25 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ChakraProvider } from "@chakra-ui/react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import type { PropsWithChildren } from "react";
-import { MemoryRouter } from "react-router-dom";
+import { useContext } from "react";
 
-export const Wrapper = ({ children }: PropsWithChildren) => {
-  const queryClient = new QueryClient({
-    defaultOptions: {
-      queries: {
-        staleTime: Infinity,
-      },
-    },
-  });
+import { TimezoneContext, type TimezoneContextType } from "./TimezoneProvider";
 
-  return (
-    <ChakraProvider>
-      <QueryClientProvider client={queryClient}>
-        <MemoryRouter>{children}</MemoryRouter>
-      </QueryClientProvider>
-    </ChakraProvider>
-  );
+export const useTimezone = (): TimezoneContextType => {
+  const context = useContext(TimezoneContext);
+
+  if (context === undefined) {
+    throw new Error("useTimezone must be used within a TimezoneProvider");
+  }
+
+  return context;
 };
diff --git a/airflow/ui/src/utils/Wrapper.tsx 
b/airflow/ui/src/layouts/Nav/TimezoneModal.tsx
similarity index 55%
copy from airflow/ui/src/utils/Wrapper.tsx
copy to airflow/ui/src/layouts/Nav/TimezoneModal.tsx
index 5dce11b333..26dfea9541 100644
--- a/airflow/ui/src/utils/Wrapper.tsx
+++ b/airflow/ui/src/layouts/Nav/TimezoneModal.tsx
@@ -16,25 +16,34 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ChakraProvider } from "@chakra-ui/react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import type { PropsWithChildren } from "react";
-import { MemoryRouter } from "react-router-dom";
+import {
+  Modal,
+  ModalOverlay,
+  ModalContent,
+  ModalHeader,
+  ModalBody,
+  ModalCloseButton,
+} from "@chakra-ui/react";
+import React from "react";
 
-export const Wrapper = ({ children }: PropsWithChildren) => {
-  const queryClient = new QueryClient({
-    defaultOptions: {
-      queries: {
-        staleTime: Infinity,
-      },
-    },
-  });
+import TimezoneSelector from "./TimezoneSelector";
 
-  return (
-    <ChakraProvider>
-      <QueryClientProvider client={queryClient}>
-        <MemoryRouter>{children}</MemoryRouter>
-      </QueryClientProvider>
-    </ChakraProvider>
-  );
+type TimezoneModalProps = {
+  isOpen: boolean;
+  onClose: () => void;
 };
+
+const TimezoneModal: React.FC<TimezoneModalProps> = ({ isOpen, onClose }) => (
+  <Modal isOpen={isOpen} onClose={onClose} size="xl">
+    <ModalOverlay />
+    <ModalContent>
+      <ModalHeader>Select Timezone</ModalHeader>
+      <ModalCloseButton />
+      <ModalBody>
+        <TimezoneSelector />
+      </ModalBody>
+    </ModalContent>
+  </Modal>
+);
+
+export default TimezoneModal;
diff --git a/airflow/ui/src/layouts/Nav/TimezoneSelector.tsx 
b/airflow/ui/src/layouts/Nav/TimezoneSelector.tsx
new file mode 100644
index 0000000000..0be59dc359
--- /dev/null
+++ b/airflow/ui/src/layouts/Nav/TimezoneSelector.tsx
@@ -0,0 +1,89 @@
+/*!
+ * 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, VStack } from "@chakra-ui/react";
+import { Select, type SingleValue } from "chakra-react-select";
+import dayjs from "dayjs";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+import React, { useMemo } from "react";
+
+import { useTimezone } from "src/context/timezone";
+
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
+type TimezoneOption = {
+  label: string;
+  value: string;
+};
+
+const TimezoneSelector: React.FC = () => {
+  const { selectedTimezone, setSelectedTimezone } = useTimezone();
+  const timezones = useMemo<Array<string>>(() => {
+    const tzList = Intl.supportedValuesOf("timeZone");
+    const guessedTz = dayjs.tz.guess();
+    const uniqueTimezones = new Set([
+      "UTC",
+      ...(guessedTz ? [guessedTz] : []),
+      ...tzList,
+    ]);
+
+    return [...uniqueTimezones];
+  }, []);
+
+  const options = useMemo<Array<TimezoneOption>>(
+    () =>
+      timezones.map((tz) => ({
+        label: tz === "UTC" ? "UTC (Coordinated Universal Time)" : tz,
+        value: tz,
+      })),
+    [timezones],
+  );
+
+  const handleTimezoneChange = (
+    selectedOption: SingleValue<TimezoneOption>,
+  ) => {
+    if (selectedOption) {
+      setSelectedTimezone(selectedOption.value);
+    }
+  };
+
+  const currentTime = dayjs()
+    .tz(selectedTimezone)
+    .format("YYYY-MM-DD HH:mm:ss");
+
+  return (
+    <VStack align="stretch" spacing={6}>
+      <Select<TimezoneOption>
+        onChange={handleTimezoneChange}
+        options={options}
+        placeholder="Select a timezone"
+        value={options.find((option) => option.value === selectedTimezone)}
+      />
+      <Box borderRadius="md" boxShadow="md" p={6}>
+        <Text fontSize="lg" fontWeight="bold">
+          Current time in {selectedTimezone}:
+        </Text>
+        <Text fontSize="2xl">{currentTime}</Text>
+      </Box>
+    </VStack>
+  );
+};
+
+export default TimezoneSelector;
diff --git a/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx 
b/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
index c43c17b6d0..d117a81700 100644
--- a/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
+++ b/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
@@ -23,13 +23,25 @@ import {
   useColorMode,
   MenuItem,
   MenuList,
+  useDisclosure,
 } from "@chakra-ui/react";
-import { FiMoon, FiSun, FiUser } from "react-icons/fi";
+import dayjs from "dayjs";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+import { FiClock, FiMoon, FiSun, FiUser } from "react-icons/fi";
 
+import { useTimezone } from "src/context/timezone";
+
+import TimezoneModal from "./TimezoneModal";
 import { navButtonProps } from "./navButtonProps";
 
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
 export const UserSettingsButton = () => {
   const { colorMode, toggleColorMode } = useColorMode();
+  const { isOpen, onClose, onOpen } = useDisclosure();
+  const { selectedTimezone } = useTimezone();
 
   return (
     <Menu placement="right">
@@ -52,7 +64,12 @@ export const UserSettingsButton = () => {
             </>
           )}
         </MenuItem>
+        <MenuItem onClick={onOpen}>
+          <FiClock size="1.25rem" style={{ marginRight: "8px" }} />
+          {dayjs().tz(selectedTimezone).format("HH:mm z (Z)")}
+        </MenuItem>
       </MenuList>
+      <TimezoneModal isOpen={isOpen} onClose={onClose} />
     </Menu>
   );
 };
diff --git a/airflow/ui/src/main.tsx b/airflow/ui/src/main.tsx
index 12434ca7ba..dda70beccd 100644
--- a/airflow/ui/src/main.tsx
+++ b/airflow/ui/src/main.tsx
@@ -24,6 +24,7 @@ import { BrowserRouter } from "react-router-dom";
 
 import { App } from "src/App";
 
+import { TimezoneProvider } from "./context/timezone";
 import theme from "./theme";
 
 const queryClient = new QueryClient({
@@ -64,7 +65,9 @@ root.render(
   <BrowserRouter basename="/webapp">
     <ChakraProvider theme={theme}>
       <QueryClientProvider client={queryClient}>
-        <App />
+        <TimezoneProvider>
+          <App />
+        </TimezoneProvider>
       </QueryClientProvider>
     </ChakraProvider>
   </BrowserRouter>,
diff --git a/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow/ui/src/pages/DagsList/DagCard.tsx
index d555abbc0c..453c22e06e 100644
--- a/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -31,6 +31,7 @@ import {
 import { FiCalendar, FiTag } from "react-icons/fi";
 
 import type { DAGResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
 import { TogglePause } from "src/components/TogglePause";
 
 type Props = {
@@ -98,7 +99,9 @@ export const DagCard = ({ dag }: Props) => {
             Next Run
           </Heading>
           {Boolean(dag.next_dagrun) ? (
-            <Text fontSize="sm">{dag.next_dagrun}</Text>
+            <Text fontSize="sm">
+              <Time datetime={dag.next_dagrun} />
+            </Text>
           ) : undefined}
           {Boolean(dag.timetable_summary) ? (
             <Tooltip hasArrow label={dag.timetable_description}>
diff --git a/airflow/ui/src/pages/DagsList/DagsList.tsx 
b/airflow/ui/src/pages/DagsList/DagsList.tsx
index 5e13248732..9d5b01e20f 100644
--- a/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -41,6 +41,7 @@ import type { CardDef } from "src/components/DataTable/types";
 import { useTableURLState } from "src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { SearchBar } from "src/components/SearchBar";
+import Time from "src/components/Time";
 import { TogglePause } from "src/components/TogglePause";
 import {
   SearchParamsKeys,
@@ -82,6 +83,10 @@ const columns: Array<ColumnDef<DAGResponse>> = [
   },
   {
     accessorKey: "next_dagrun",
+    cell: ({ row: { original } }) =>
+      Boolean(original.next_dagrun) ? (
+        <Time datetime={original.next_dagrun} />
+      ) : undefined,
     enableSorting: false,
     header: "Next DAG Run",
   },
diff --git a/airflow/ui/src/pages/Dashboard/HealthTag.tsx 
b/airflow/ui/src/pages/Dashboard/HealthTag.tsx
index 7318f077da..9022c42390 100644
--- a/airflow/ui/src/pages/Dashboard/HealthTag.tsx
+++ b/airflow/ui/src/pages/Dashboard/HealthTag.tsx
@@ -18,6 +18,7 @@
  */
 import { Skeleton, Tag, TagLabel, Text, Tooltip } from "@chakra-ui/react";
 
+import Time from "src/components/Time";
 import { capitalize } from "src/utils";
 
 export const HealthTag = ({
@@ -42,7 +43,9 @@ export const HealthTag = ({
       label={
         <div>
           <Text>Status: {capitalize(status)}</Text>
-          <Text>Last Heartbeat: {latestHeartbeat}</Text>
+          <Text>
+            Last Heartbeat: <Time datetime={latestHeartbeat} />
+          </Text>
         </div>
       }
       shouldWrapChildren
diff --git a/airflow/ui/src/utils/Wrapper.tsx b/airflow/ui/src/utils/Wrapper.tsx
index 5dce11b333..5056b4d5bb 100644
--- a/airflow/ui/src/utils/Wrapper.tsx
+++ b/airflow/ui/src/utils/Wrapper.tsx
@@ -21,6 +21,8 @@ import { QueryClient, QueryClientProvider } from 
"@tanstack/react-query";
 import type { PropsWithChildren } from "react";
 import { MemoryRouter } from "react-router-dom";
 
+import { TimezoneProvider } from "src/context/timezone";
+
 export const Wrapper = ({ children }: PropsWithChildren) => {
   const queryClient = new QueryClient({
     defaultOptions: {
@@ -33,7 +35,9 @@ export const Wrapper = ({ children }: PropsWithChildren) => {
   return (
     <ChakraProvider>
       <QueryClientProvider client={queryClient}>
-        <MemoryRouter>{children}</MemoryRouter>
+        <MemoryRouter>
+          <TimezoneProvider>{children}</TimezoneProvider>
+        </MemoryRouter>
       </QueryClientProvider>
     </ChakraProvider>
   );
diff --git a/airflow/ui/tsconfig.app.json b/airflow/ui/tsconfig.app.json
index d41468d9d3..d19c8a8651 100644
--- a/airflow/ui/tsconfig.app.json
+++ b/airflow/ui/tsconfig.app.json
@@ -1,9 +1,9 @@
 {
   "$schema": "https://json.schemastore.org/tsconfig";,
   "compilerOptions": {
-    "target": "ES2020",
+    "target": "ES2022",
     "useDefineForClassFields": true,
-    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
     "module": "ESNext",
     "skipLibCheck": true,
 

Reply via email to