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,