Skip to content

Commit 15f09c7

Browse files
authored
Add timezone selection to new UI (#43132)
1 parent aeb7e90 commit 15f09c7

File tree

16 files changed

+425
-7
lines changed

16 files changed

+425
-7
lines changed

airflow/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@tanstack/react-table": "^8.20.1",
2525
"axios": "^1.7.7",
2626
"chakra-react-select": "^4.9.2",
27+
"dayjs": "^1.11.13",
2728
"framer-motion": "^11.3.29",
2829
"react": "^18.3.1",
2930
"react-dom": "^18.3.1",

airflow/ui/pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { render, screen } from "@testing-library/react";
20+
import dayjs from "dayjs";
21+
import { describe, it, expect, vi } from "vitest";
22+
23+
import { TimezoneContext } from "src/context/timezone";
24+
import { Wrapper } from "src/utils/Wrapper";
25+
26+
import Time, { defaultFormat, defaultFormatWithTZ } from "./Time";
27+
28+
describe("Test Time and TimezoneProvider", () => {
29+
it("Displays a UTC time correctly", () => {
30+
const now = new Date();
31+
32+
render(<Time datetime={now.toISOString()} />, {
33+
wrapper: Wrapper,
34+
});
35+
36+
const utcTime = screen.getByText(dayjs.utc(now).format(defaultFormat));
37+
38+
expect(utcTime).toBeDefined();
39+
expect(utcTime.title).toBeFalsy();
40+
});
41+
42+
it("Displays a set timezone, includes UTC date in title", () => {
43+
const now = new Date();
44+
const tz = "US/Samoa";
45+
46+
render(
47+
<TimezoneContext.Provider
48+
value={{ selectedTimezone: tz, setSelectedTimezone: vi.fn() }}
49+
>
50+
<Time datetime={now.toISOString()} />
51+
</TimezoneContext.Provider>,
52+
{
53+
wrapper: Wrapper,
54+
},
55+
);
56+
57+
const samoaTime = screen.getByText(dayjs(now).tz(tz).format(defaultFormat));
58+
59+
expect(samoaTime).toBeDefined();
60+
expect(samoaTime.title).toEqual(
61+
dayjs().tz("UTC").format(defaultFormatWithTZ),
62+
);
63+
});
64+
});

airflow/ui/src/components/Time.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import dayjs from "dayjs";
20+
import advancedFormat from "dayjs/plugin/advancedFormat";
21+
import tz from "dayjs/plugin/timezone";
22+
import utc from "dayjs/plugin/utc";
23+
24+
import { useTimezone } from "src/context/timezone";
25+
26+
export const defaultFormat = "YYYY-MM-DD, HH:mm:ss";
27+
export const defaultFormatWithTZ = `${defaultFormat} z`;
28+
export const defaultTZFormat = "z (Z)";
29+
30+
dayjs.extend(utc);
31+
dayjs.extend(tz);
32+
dayjs.extend(advancedFormat);
33+
34+
type Props = {
35+
readonly datetime?: string | null;
36+
readonly format?: string;
37+
};
38+
39+
const Time = ({ datetime, format = defaultFormat }: Props) => {
40+
const { selectedTimezone } = useTimezone();
41+
const time = dayjs(datetime);
42+
43+
if (datetime === null || datetime === undefined || !time.isValid()) {
44+
return undefined;
45+
}
46+
47+
const formattedTime = time.tz(selectedTimezone).format(format);
48+
const utcTime = time.tz("UTC").format(defaultFormatWithTZ);
49+
50+
return (
51+
<time
52+
dateTime={datetime}
53+
// show title if date is not UTC
54+
title={selectedTimezone.toUpperCase() === "UTC" ? undefined : utcTime}
55+
>
56+
{formattedTime}
57+
</time>
58+
);
59+
};
60+
61+
export default Time;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import {
20+
createContext,
21+
useState,
22+
useMemo,
23+
type PropsWithChildren,
24+
} from "react";
25+
26+
export type TimezoneContextType = {
27+
selectedTimezone: string;
28+
setSelectedTimezone: (timezone: string) => void;
29+
};
30+
31+
export const TimezoneContext = createContext<TimezoneContextType | undefined>(
32+
undefined,
33+
);
34+
35+
const TIMEZONE_KEY = "timezone";
36+
37+
export const TimezoneProvider = ({ children }: PropsWithChildren) => {
38+
const [selectedTimezone, setSelectedTimezone] = useState(() => {
39+
const timezone = localStorage.getItem(TIMEZONE_KEY);
40+
41+
return timezone ?? "UTC";
42+
});
43+
44+
const selectTimezone = (tz: string) => {
45+
localStorage.setItem(TIMEZONE_KEY, tz);
46+
setSelectedTimezone(tz);
47+
};
48+
49+
const value = useMemo<TimezoneContextType>(
50+
() => ({ selectedTimezone, setSelectedTimezone: selectTimezone }),
51+
[selectedTimezone],
52+
);
53+
54+
return (
55+
<TimezoneContext.Provider value={value}>
56+
{children}
57+
</TimezoneContext.Provider>
58+
);
59+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
export * from "./TimezoneProvider";
21+
export * from "./useTimezone";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { useContext } from "react";
20+
21+
import { TimezoneContext, type TimezoneContextType } from "./TimezoneProvider";
22+
23+
export const useTimezone = (): TimezoneContextType => {
24+
const context = useContext(TimezoneContext);
25+
26+
if (context === undefined) {
27+
throw new Error("useTimezone must be used within a TimezoneProvider");
28+
}
29+
30+
return context;
31+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import {
20+
Modal,
21+
ModalOverlay,
22+
ModalContent,
23+
ModalHeader,
24+
ModalBody,
25+
ModalCloseButton,
26+
} from "@chakra-ui/react";
27+
import React from "react";
28+
29+
import TimezoneSelector from "./TimezoneSelector";
30+
31+
type TimezoneModalProps = {
32+
isOpen: boolean;
33+
onClose: () => void;
34+
};
35+
36+
const TimezoneModal: React.FC<TimezoneModalProps> = ({ isOpen, onClose }) => (
37+
<Modal isOpen={isOpen} onClose={onClose} size="xl">
38+
<ModalOverlay />
39+
<ModalContent>
40+
<ModalHeader>Select Timezone</ModalHeader>
41+
<ModalCloseButton />
42+
<ModalBody>
43+
<TimezoneSelector />
44+
</ModalBody>
45+
</ModalContent>
46+
</Modal>
47+
);
48+
49+
export default TimezoneModal;

0 commit comments

Comments
 (0)