/* * Copyright (C) 2025 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import React, { useEffect, useState } from "react"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js"; import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js"; import { Modal, ModalBody, ModalFooter, ModalHeader } from "@patternfly/react-core/dist/esm/components/Modal/index.js"; import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js"; import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js"; import { ValidatedOptions } from "@patternfly/react-core/dist/esm/helpers/constants.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js"; import { TrashIcon } from "@patternfly/react-icons/dist/esm/icons/trash-icon"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table/dist/esm/index.js"; import { checkNTPServer, getTimeServersFromConfig, getTimeSources, setTimeSources } from "../../apis/timezone.js"; const _ = cockpit.gettext; const SCREEN_ID = "anaconda-screen-date-time"; const NTP_REGEX = /^[a-z0-9.-]+\.[a-z]{2,}$/i; const hasNtsOption = (src) => (src.options?.v ?? []).includes("nts"); const CustomNTPModal = ({ onClose }) => { const [errors, setErrors] = useState(); const [ntpSources, setNtpSources] = useState([]); useEffect(() => { const loadNtpSources = async () => { let srcs = await getTimeSources(); const ntps = srcs.filter((s) => s.type?.v === "SERVER" || s.type?.v === "POOL"); if (ntps.length) { srcs = ntps; } else { srcs = await getTimeServersFromConfig(); } srcs = srcs.map(src => ({ ...src, isAvailable: true, // Assume all sources are available initially isCommitted: true, })); setNtpSources(srcs); // Check if the sources are available srcs.map(async src => { const isAvailable = await checkNTPServer({ hostname: src.hostname.v, isNTS: hasNtsOption(src), }); return ({ ...src, isAvailable }); }); setNtpSources(srcs); }; loadNtpSources(); }, []); const handleEdit = async ({ hostname, index, isPool, ntsEnabled, validateServer }) => { setErrors(errors => ({ ...errors, [index]: undefined })); const source = ntpSources[index]; setNtpSources((prev) => { const updated = [...prev]; const row = { ...updated[index], hostname: { v: hostname !== undefined ? hostname : source.hostname.v }, isAvailable: true, // Reset availability while editing type: { v: isPool !== undefined ? (isPool ? "POOL" : "SERVER") : source.type.v }, }; if (ntsEnabled !== undefined) { const opts = [...(source.options?.v ?? [])].filter(o => o !== "nts"); if (ntsEnabled) { opts.push("nts"); } row.options = { v: opts }; } updated[index] = row; return updated; }); if ((hostname || source.hostname.v) && validateServer) { if (!NTP_REGEX.test(hostname !== undefined ? hostname : source.hostname.v)) { setErrors(errors => ({ ...errors, [index]: _("Invalid hostname") })); } else { setNtpSources(prev => { const updated = [...prev]; updated[index] = { ...updated[index], isAvailable: undefined, }; return updated; }); const isAvailable = await checkNTPServer({ hostname: hostname !== undefined ? hostname : source.hostname.v, isNTS: ntsEnabled !== undefined ? ntsEnabled : hasNtsOption(source), }); setNtpSources(prev => { const updated = [...prev]; updated[index] = { ...updated[index], isAvailable, }; return updated; }); } } }; const handleAdd = () => { setNtpSources([ ...ntpSources, { hostname: { v: "" }, isAvailable: true, options: { v: ["iburst"] }, type: { v: "POOL" } } ]); }; const handleRemove = (idx) => { setNtpSources(ntpSources.filter((_, i) => i !== idx)); }; const handleConfirm = async () => { await setTimeSources({ sources: ntpSources.map(s => ({ hostname: cockpit.variant("s", s.hostname.v), options: cockpit.variant("as", s.options.v), type: cockpit.variant("s", s.type.v), })) }); onClose(); }; return ( {ntpSources.map((src, index) => ( ))}
{_("Server")} {_("Options")}
} id={SCREEN_ID + "-ntp-table-row-" + index + "-hostname"} validated={ (errors?.[index] ? ValidatedOptions.error : ValidatedOptions.default) } value={src.hostname.v} onChange={(e) => handleEdit({ hostname: e.target.value, index })} onBlur={(e) => handleEdit({ hostname: e.target.value, index, validateServer: true })} {...(src.isCommitted ? { readOnlyVariant: "plain" } : {})} /> {(errors?.[index] || src.isAvailable === false) && {errors?.[index] && {errors[index]} } {src.isAvailable === false && {_("Server is not reachable")} } } handleEdit({ index, isPool: _event.target.checked })} /> handleEdit({ index, ntsEnabled: !hasNtsOption(src), validateServer: true })} />
); }; export const CustomNTP = ({ autoDateTime }) => { const [showCustomNtpModal, setShowCustomNtpModal] = useState(false); return ( <> {showCustomNtpModal && setShowCustomNtpModal(false)} />} ); };