* Copyright (C) 2025 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import { DateTime } from "luxon";
import React, { useCallback, useContext, useEffect, useState } from "react";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
import { Title } from "@patternfly/react-core/dist/esm/components/Title/index.js";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import {
getNTPEnabled,
getSystemDateTime,
setNTPEnabled,
setSystemDateTime,
} from "../../apis/timezone.js";
import { convertToCockpitLang } from "../../helpers/language.js";
import {
formatDateInput, getLocalTimeForPicker, getUserLocale,
is24HourLocale,
isValidDate,
} from "../../helpers/timezone.js";
import { LanguageContext, NetworkContext, PageContext } from "../../contexts/Common.jsx";
import { CustomNTP } from "./CustomNTPModal.jsx";
import { TimezoneSection } from "./Timezone.jsx";
import "./DateAndTime.scss";
const _ = cockpit.gettext;
const SCREEN_ID = "anaconda-screen-date-time";
const DateAndTimeSection = ({ locale, setSectionValid, timezone }) => {
const [autoDateTime, setAutoDateTime] = useState(true);
const [isDateTimeValid, setIsDateTimeValid] = useState(true);
const [datetime, setDatetime] = useState(DateTime.utc());
const isConnected = useContext(NetworkContext).connected;
const [timeFormat, setTimeFormat] = useState(() =>
is24HourLocale(locale) ? "24" : "ampm"
);
useEffect(() => {
const updateTimeDateData = async () => {
const ntpEnabled = await getNTPEnabled();
setAutoDateTime(!!ntpEnabled);
const dtIso = await getSystemDateTime();
if (dtIso) {
setDatetime(
DateTime.fromISO(dtIso, { zone: "utc" }).setZone(timezone)
);
}
};
updateTimeDateData();
}, [timezone]);
const handleAutoDateTime = useCallback(() => {
const newValue = !autoDateTime;
setAutoDateTime(newValue);
setNTPEnabled({ enabled: newValue });
}, [autoDateTime]);
useEffect(() => {
setSectionValid(autoDateTime || isDateTimeValid);
}, [autoDateTime, isDateTimeValid, setSectionValid]);
const handleDateChange = (_event, _, newDate) => {
const isValid = !!newDate && isValidDate(newDate);
setIsDateTimeValid(isValid);
if (!isValid) return;
const localDate = DateTime.fromJSDate(newDate);
const datePart = localDate.setZone(timezone, { keepLocalTime: true });
const combined = datetime.set({
day: datePart.day,
month: datePart.month,
year: datePart.year,
});
setDatetime(combined);
if (!autoDateTime && combined.isValid) {
setSystemDateTime({ dateTimeSpec: combined.toISO() });
}
};
const handleTimeChange = (event, _, hour, minute, seconds, isValid) => {
const timeIsEmpty = hour === null || minute === null;
const valid = isValid && !timeIsEmpty;
setIsDateTimeValid(valid);
if (!valid) return;
if (typeof hour !== "number" || typeof minute !== "number") return;
const updated = datetime.set({
hour,
minute,
second: seconds || 0,
});
setDatetime(updated);
if (!autoDateTime && updated.isValid) {
setSystemDateTime({ dateTimeSpec: updated.toISO() });
}
};
const datePickerValue = formatDateInput(datetime);
const timePickerValue = getLocalTimeForPicker(datetime, timeFormat === "24");
const autoDateTimeCheckbox = (
<Checkbox
disabled={!isConnected}
id={`${SCREEN_ID}-auto-date-time`}
isChecked={autoDateTime}
label={_("Automatically set date and time, using time servers")}
onChange={handleAutoDateTime}
/>
);
const autoDateTimeItem = isConnected
? autoDateTimeCheckbox
: (
<Popover
id={`${SCREEN_ID}-auto-date-time-popover`}
bodyContent={_("You must be connected to a network to enable automatic date and time.")}
triggerAction="hover"
>
{autoDateTimeCheckbox}
</Popover>
);
return (
<>
<Title headingLevel="h2">{_("Date and time")}</Title>
<FormGroup>
<Stack hasGutter>
<StackItem>
<Flex alignItems={{ default: "alignItemsCenter" }}>
<FlexItem>
{autoDateTimeItem}
</FlexItem>
<FlexItem>
<CustomNTP autoDateTime={autoDateTime} />
</FlexItem>
</Flex>
</StackItem>
<StackItem>
<Flex alignItems={{ default: "alignItemsCenter" }} className={`${SCREEN_ID}__indent`}>
<FlexItem>
<DatePicker
id={`${SCREEN_ID}-date`}
value={datePickerValue}
onChange={handleDateChange}
isDisabled={autoDateTime}
aria-label={_("Date")}
/>
</FlexItem>
<FlexItem>
<TimePicker
id={`${SCREEN_ID}-time`}
key={timeFormat + timezone}
time={timePickerValue}
onChange={handleTimeChange}
is24Hour={timeFormat === "24"}
isDisabled={autoDateTime}
aria-label={_("Time")}
placeholder={timeFormat === "24" ? "HH:MM" : "hh:mm AM/PM"}
/>
</FlexItem>
<FlexItem>
<Switch
id={`${SCREEN_ID}-show-ampm`}
isChecked={timeFormat === "ampm"}
onChange={() => setTimeFormat(timeFormat === "24" ? "ampm" : "24")}
label={_("Show AM/PM")}
/>
</FlexItem>
</Flex>
</StackItem>
</Stack>
</FormGroup>
</>
);
};
export const DateAndTimePage = ({ dispatch }) => {
const { setIsFormValid } = useContext(PageContext) ?? {};
const [dateTimeValid, setDateTimeValid] = useState(false);
const [timezoneLabel, setTimezoneLabel] = useState("");
const [timezoneValid, setTimezoneValid] = useState(false);
const { language } = useContext(LanguageContext);
const locale = convertToCockpitLang({ lang: language || getUserLocale() });
useEffect(() => {
dispatch({ type: "SET_TIMEZONE_USER_CONFIGURED" });
}, [dispatch]);
useEffect(() => {
setIsFormValid(dateTimeValid && timezoneValid);
}, [dateTimeValid, timezoneValid, setIsFormValid]);
return (
<Form>
<DateAndTimeSection locale={locale} setSectionValid={setDateTimeValid} timezone={timezoneLabel} />
<TimezoneSection locale={locale} setSectionValid={setTimezoneValid} setTimezoneLabel={setTimezoneLabel} />
</Form>
);
};