Skip to content
Merged
14 changes: 10 additions & 4 deletions airflow/ui/src/components/AppContainer/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,15 @@ import {
MdBrightness2,
MdAccountCircle,
MdExitToApp,
MdQueryBuilder,
} from 'react-icons/md';
import dayjs from 'dayjs';
import tz from 'dayjs/plugin/timezone';

import { useAuthContext } from 'providers/auth/context';
import { useDateContext, HOURS_24 } from 'providers/DateProvider';

import ApacheAirflowLogo from 'components/icons/ApacheAirflowLogo';
import TimezoneDropdown from './TimezoneDropdown';

dayjs.extend(tz);

interface Props {
bodyBg: string;
overlayBg: string;
Expand All @@ -55,6 +53,7 @@ interface Props {

const AppHeader: React.FC<Props> = ({ bodyBg, overlayBg, breadcrumb }) => {
const { toggleColorMode } = useColorMode();
const { dateFormat, toggle24Hour } = useDateContext();
const headerHeight = '56px';
const { hasValidAuthToken, logout } = useAuthContext();
const darkLightIcon = useColorModeValue(MdBrightness2, MdWbSunny);
Expand Down Expand Up @@ -102,6 +101,13 @@ const AppHeader: React.FC<Props> = ({ bodyBg, overlayBg, breadcrumb }) => {
{darkLightText}
Mode
</MenuItem>
{/* Clock config should move to User Profile Settings when that page exists */}
<MenuItem onClick={toggle24Hour}>
<Icon as={MdQueryBuilder} mr="2" />
Use
{dateFormat === HOURS_24 ? ' 12 hour ' : ' 24 hour '}
clock
</MenuItem>
<MenuDivider />
<MenuItem onClick={logout}>
<Icon as={MdExitToApp} mr="2" />
Expand Down
16 changes: 5 additions & 11 deletions airflow/ui/src/components/AppContainer/TimezoneDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import React, { useState, useRef } from 'react';
import React, { useRef } from 'react';
import {
Box,
Button,
Expand All @@ -26,20 +26,15 @@ import {
MenuList,
Tooltip,
} from '@chakra-ui/react';
import dayjs from 'dayjs';
import tz from 'dayjs/plugin/timezone';
import { getTimeZones } from '@vvo/tzdb';

import Select from 'components/MultiSelect';
import { useTimezoneContext } from 'providers/TimezoneProvider';

dayjs.extend(tz);
import { useDateContext } from 'providers/DateProvider';

interface Option { value: string, label: string }

const TimezoneDropdown: React.FC = () => {
const { timezone, setTimezone } = useTimezoneContext();
const [now, setNow] = useState(dayjs().tz(timezone));
const { timezone, setTimezone, formatDate } = useDateContext();
const menuRef = useRef<HTMLButtonElement>(null);

const timezones = getTimeZones();
Expand All @@ -54,7 +49,6 @@ const TimezoneDropdown: React.FC = () => {
const onChangeTimezone = (newTimezone: Option | null) => {
if (newTimezone) {
setTimezone(newTimezone.value);
setNow(dayjs().tz(newTimezone.value));
// Close the dropdown on a successful change
menuRef?.current?.click();
}
Expand All @@ -66,10 +60,10 @@ const TimezoneDropdown: React.FC = () => {
<MenuButton as={Button} variant="ghost" mr="4" ref={menuRef}>
<Box
as="time"
dateTime={now.toString()}
dateTime={formatDate()}
fontSize="md"
>
{now.format('HH:mm Z')}
{formatDate()}
</Box>
</MenuButton>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,41 +27,61 @@ import dayjsTz from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(dayjsTz);

interface TimezoneContextData {
export const HOURS_24 = 'HH:mm Z';
export const HOURS_12 = 'h:mmA Z';

interface DateContextData {
timezone: string;
setTimezone: (value: string) => void;
dateFormat: string;
toggle24Hour: () => void;
formatDate: (date?: string | Date) => string;
}

export const TimezoneContext = createContext<TimezoneContextData>({
export const DateContext = createContext<DateContextData>({
timezone: 'UTC',
setTimezone: () => {},
dateFormat: HOURS_24,
toggle24Hour: () => {},
formatDate: () => '',
});

export const useTimezoneContext = () => useContext(TimezoneContext);
export const useDateContext = () => useContext(DateContext);

type Props = {
children: ReactNode;
};

const TimezoneProvider = ({ children }: Props): ReactElement => {
const DateProvider = ({ children }: Props): ReactElement => {
// TODO: add in default_timezone when GET /ui-metadata is available
// guess timezone on browser or default to utc and don't guess when testing
const isTest = process.env.NODE_ENV === 'test';
const [timezone, setTimezone] = useState(isTest ? 'UTC' : dayjs.tz.guess());
const [dateFormat, setFormat] = useState(HOURS_24);

const toggle24Hour = () => {
setFormat(dateFormat === HOURS_24 ? HOURS_12 : HOURS_24);
};

useEffect(() => {
dayjs.tz.setDefault(timezone);
}, [timezone]);

const formatDate = (date?: string | Date) => dayjs(date).tz(timezone).format(dateFormat);

return (
<TimezoneContext.Provider
<DateContext.Provider
value={{
timezone,
setTimezone,
dateFormat,
toggle24Hour,
formatDate,
}}
>
{children}
</TimezoneContext.Provider>
</DateContext.Provider>
);
};

export default TimezoneProvider;
export default DateProvider;
6 changes: 3 additions & 3 deletions airflow/ui/src/providers/auth/PrivateRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import React, {
import { Route, RouteProps } from 'react-router-dom';

import Login from 'views/Login';
// TimezoneProvider has to be used after authentication
import TimezoneProvider from 'providers/TimezoneProvider';
// DateProvider has to be used after authentication
import DateProvider from 'providers/DateProvider';
import { useAuthContext } from './context';

const PrivateRoute: FC<RouteProps> = (props) => {
const { hasValidAuthToken } = useAuthContext();
return hasValidAuthToken ? <TimezoneProvider><Route {...props} /></TimezoneProvider> : <Login />;
return hasValidAuthToken ? <DateProvider><Route {...props} /></DateProvider> : <Login />;
};

export default PrivateRoute;
6 changes: 0 additions & 6 deletions airflow/ui/test/Login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,17 @@ import { render, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import nock from 'nock';
import axios from 'axios';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';

import Login from 'views/Login';
import App from 'App';
import AuthProvider from 'providers/auth/AuthProvider';

import { url, defaultHeaders, QueryWrapper } from './utils';

dayjs.extend(utc);
dayjs.extend(timezone);
axios.defaults.adapter = require('axios/lib/adapters/http');

describe('test login component', () => {
beforeAll(() => {
dayjs.tz.setDefault('UTC');
nock(url)
.defaultReplyHeaders(defaultHeaders)
.persist()
Expand Down
10 changes: 5 additions & 5 deletions airflow/ui/test/TimezoneDropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';

import TimezoneDropdown from 'components/AppContainer/TimezoneDropdown';
import TimezoneProvider from 'providers/TimezoneProvider';
import DateProvider, { HOURS_24 } from 'providers/DateProvider';
import { ChakraWrapper } from './utils';

dayjs.extend(utc);
Expand All @@ -35,13 +35,13 @@ dayjs.extend(timezone);
describe('test timezone dropdown', () => {
test('Can search for a new timezone and the date changes', () => {
const { getByText } = render(
<TimezoneProvider>
<DateProvider>
<TimezoneDropdown />
</TimezoneProvider>,
</DateProvider>,
{ wrapper: ChakraWrapper },
);

const initialTime = dayjs().tz('UTC').format('HH:mm Z');
const initialTime = dayjs().tz('UTC').format(HOURS_24);

expect(getByText(initialTime)).toBeInTheDocument();
const button = getByText(initialTime);
Expand All @@ -54,6 +54,6 @@ describe('test timezone dropdown', () => {
expect(option).toBeInTheDocument();
fireEvent.click(option);

expect(getByText(dayjs().tz('America/Anchorage').format('HH:mm Z'))).toBeInTheDocument();
expect(getByText(dayjs().tz('America/Anchorage').format(HOURS_24))).toBeInTheDocument();
});
});