LCOV - code coverage report
Current view: top level - pkg/lib - serverTime.js Coverage Total Hit
Test: cockpit Lines: 91.8 % 601 552
Test Date: 2026-07-17 07:33:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2019 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5          111 : import cockpit from "cockpit";
       6          111 : import React from "react";
       7              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
       8              : import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
       9              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      10              : import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
      11              : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
      12              : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
      13              : import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
      14              : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
      15              : import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
      16              : import { CloseIcon, ExclamationCircleIcon, InfoCircleIcon, PlusIcon } from "@patternfly/react-icons";
      17              : import { show_modal_dialog } from "cockpit-components-dialog.jsx";
      18              : import { SimpleSelect } from "cockpit-components-simple-select";
      19              : import { TypeaheadSelect } from "cockpit-components-typeahead-select";
      20              : import { useObject, useEvent } from "hooks.js";
      21              : 
      22              : import * as service from "service.js";
      23              : import * as timeformat from "timeformat";
      24              : import * as python from "python.js";
      25              : import get_timesync_backend_py from "./get-timesync-backend.py";
      26              : 
      27              : import { superuser } from "superuser.js";
      28              : 
      29              : import "serverTime.scss";
      30              : 
      31          111 : const _ = cockpit.gettext;
      32              : 
      33           92 : export function ServerTime() {
      34           92 :     const self = this;
      35           92 :     cockpit.event_target(self);
      36              : 
      37           85 :     function emit_changed() {
      38           85 :         self.dispatchEvent("changed");
      39           85 :     }
      40              : 
      41           92 :     let time_offset = null;
      42           92 :     let remote_offset = null;
      43              : 
      44           92 :     let client = null;
      45           92 :     let timedate = null;
      46              : 
      47           92 :     function connect() {
      48           24 :         if (client) {
      49           24 :             timedate.removeEventListener("changed", emit_changed);
      50           24 :             client.close();
      51           24 :         }
      52           92 :         client = cockpit.dbus('org.freedesktop.timedate1', { superuser: "try" });
      53           92 :         timedate = client.proxy();
      54           92 :         timedate.addEventListener("changed", emit_changed);
      55           92 :         client.subscribe({
      56           92 :             interface: "org.freedesktop.DBus.Properties",
      57           92 :             member: "PropertiesChanged"
      58           92 :         }, ntp_updated);
      59           92 :     }
      60              : 
      61           92 :     const timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
      62           92 :     const timesyncd_service = service.proxy("systemd-timesyncd.service");
      63           92 :     const chronyd_service = service.proxy("chronyd.service");
      64              : 
      65           92 :     timesyncd_service.addEventListener("changed", emit_changed);
      66           92 :     chronyd_service.addEventListener("changed", emit_changed);
      67              : 
      68              :     /*
      69              :      * The time we return from here as its UTC time set to the
      70              :      * server time. This is the only way to get predictable
      71              :      * behavior.
      72              :      */
      73           92 :     Object.defineProperty(self, 'utc_fake_now', {
      74           92 :         enumerable: true,
      75           92 :         get: function get() {
      76           92 :             const offset = time_offset + remote_offset;
      77           92 :             return new Date(offset + (new Date()).valueOf());
      78           92 :         }
      79           92 :     });
      80              : 
      81           92 :     Object.defineProperty(self, 'now', {
      82           92 :         enumerable: true,
      83            1 :         get: function get() {
      84            1 :             return new Date(time_offset + (new Date()).valueOf());
      85            1 :         }
      86           92 :     });
      87              : 
      88           92 :     const updateInterval = window.setInterval(emit_changed, 30000);
      89              : 
      90            2 :     self.wait = function wait() {
      91            2 :         if (remote_offset === null)
      92            2 :             return self.update();
      93            0 :         return Promise.resolve();
      94            2 :     };
      95              : 
      96           92 :     self.update = function update() {
      97           92 :         return cockpit.spawn(["date", "+%s:%z"], { err: "message" })
      98           85 :                 .then(data => {
      99           85 :                     const parts = data.trim().split(":");
     100           85 :                     const timems = parseInt(parts[0], 10) * 1000;
     101           85 :                     let tzmin = parseInt(parts[1].slice(-2), 10);
     102           85 :                     const tzhour = parseInt(parts[1].slice(0, -2));
     103           85 :                     if (tzhour < 0)
     104           16 :                         tzmin = -tzmin;
     105           85 :                     const offsetms = (tzhour * 3600000) + tzmin * 60000;
     106           85 :                     const now = new Date();
     107           85 :                     time_offset = (timems - now.valueOf());
     108           85 :                     remote_offset = offsetms;
     109           85 :                     emit_changed();
     110           85 :                 })
     111            0 :                 .catch(ex => console.log("Couldn't calculate server time offset: " + cockpit.message(ex)));
     112           92 :     };
     113              : 
     114              :     /* There is no way to make sense of this date without a round trip to the
     115              :      * server, as the timezone is really server specific. */
     116            1 :     self.change_time = (datestr, timestr) => cockpit.spawn(["date", "--date=" + datestr + " " + timestr, "+%s"])
     117            1 :             .then(data => {
     118            1 :                 const seconds = parseInt(data.trim(), 10);
     119            1 :                 return timedate.call('SetTime', [seconds * 1000 * 1000, false, true])
     120            1 :                         .then(self.update);
     121            1 :             });
     122              : 
     123            0 :     self.bump_time = function (millis) {
     124            0 :         return timedate.call('SetTime', [millis, true, true]);
     125            0 :     };
     126              : 
     127           92 :     self.get_time_zone = function () {
     128           92 :         return timedate.Timezone;
     129           92 :     };
     130              : 
     131            3 :     self.set_time_zone = function (tz) {
     132            3 :         return timedate.call('SetTimezone', [tz, true]);
     133            3 :     };
     134              : 
     135           34 :     self.poll_ntp_synchronized = () => client.call(
     136           34 :         timedate.path, "org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTPSynchronized"])
     137           31 :             .then(result => {
     138           31 :                 const ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
     139           31 :                 const data = { };
     140           31 :                 data[timedate.path] = ifaces;
     141           31 :                 client.notify(data);
     142           31 :             })
     143            1 :             .catch(error => {
     144            1 :                 if (error.name != "org.freedesktop.DBus.Error.UnknownProperty" &&
     145            1 :                         error.problem != "not-found")
     146            1 :                     console.log("can't get NTPSynchronized property", error);
     147            1 :             });
     148              : 
     149           92 :     let ntp_waiting_value = null;
     150           92 :     let ntp_waiting_resolve = null;
     151              : 
     152            3 :     function ntp_updated(path, iface, member, args) {
     153            3 :         if (!ntp_waiting_resolve || !args[1].NTP)
     154            3 :             return;
     155            3 :         if (ntp_waiting_value !== args[1].NTP.v)
     156            0 :             console.warn("Unexpected value of NTP");
     157            3 :         ntp_waiting_resolve();
     158            3 :         ntp_waiting_resolve = null;
     159            3 :     }
     160              : 
     161            3 :     self.set_ntp = function set_ntp(val) {
     162            3 :         const promise = new Promise((resolve, reject) => {
     163            3 :             ntp_waiting_resolve = resolve;
     164            3 :         });
     165            3 :         ntp_waiting_value = val;
     166            3 :         client.call(timedate.path,
     167            3 :                     "org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTP"])
     168            3 :                 .then(result => {
     169              :                 // Check if don't want to enable enabled or disable disabled
     170            1 :                     if (result[0].v === val) {
     171            1 :                         ntp_waiting_resolve();
     172            1 :                         ntp_waiting_resolve = null;
     173            1 :                         return;
     174            1 :                     }
     175            3 :                     timedate.call('SetNTP', [val, true])
     176            0 :                             .catch(e => {
     177            0 :                                 ntp_waiting_resolve();
     178            0 :                                 ntp_waiting_resolve = null;
     179            0 :                                 console.error("Failed to call SetNTP:", e.message); // not-covered: OS error
     180            0 :                             });
     181            3 :                 });
     182            3 :         return promise;
     183            3 :     };
     184              : 
     185            4 :     self.get_ntp_active = function () {
     186            4 :         return timedate.NTP;
     187            4 :     };
     188              : 
     189            4 :     self.get_ntp_supported = function () {
     190            4 :         return timedate.CanNTP;
     191            4 :     };
     192              : 
     193           92 :     self.get_ntp_status = function () {
     194           92 :         const status = {
     195           92 :             initialized: false,
     196           92 :             active: false,
     197           92 :             synch: false,
     198           92 :             service: null,
     199           92 :             server: null,
     200           92 :             sub_status: null
     201           92 :         };
     202              : 
     203              :         // flag for tests that timedated/timesyncd proxies got initialized
     204           92 :         if (timedate.CanNTP !== undefined &&
     205           74 :             timedate1_service.unit && timedate1_service.unit.Id &&
     206           74 :             timesyncd_service.exists !== null &&
     207           74 :             chronyd_service.exists !== null)
     208           74 :             status.initialized = true;
     209              : 
     210           92 :         status.active = timedate.NTP;
     211           92 :         status.synch = timedate.NTPSynchronized;
     212              : 
     213           92 :         const timesyncd_server_regex = /.*time server (.*)\./i;
     214              : 
     215           92 :         const timesyncd_status = (timesyncd_service.state == "running" &&
     216           17 :                                 timesyncd_service.service?.StatusText);
     217              : 
     218           92 :         if (timesyncd_service.state == "running")
     219           17 :             status.service = "systemd-timesyncd.service";
     220           92 :         else if (chronyd_service.state == "running")
     221           74 :             status.service = "chronyd.service";
     222              : 
     223           17 :         if (timesyncd_status) {
     224           17 :             const match = timesyncd_status.match(timesyncd_server_regex);
     225           17 :             if (match)
     226           16 :                 status.server = match[1];
     227           17 :             else if (timesyncd_status != "Idle." && timesyncd_status !== "")
     228           17 :                 status.sub_status = timesyncd_status;
     229           17 :         }
     230              : 
     231           92 :         return status;
     232           92 :     };
     233              : 
     234            4 :     function get_timesync_backend() {
     235            4 :         return python.spawn(get_timesync_backend_py, [], { superuser: "try", err: "message" })
     236            4 :                 .then(data => {
     237            4 :                     const unit = data.trim();
     238            4 :                     if (unit == "systemd-timesyncd.service")
     239            0 :                         return "timesyncd";
     240            3 :                     else if (unit == "chrony.service" || unit == "chronyd.service")
     241            0 :                         return "chronyd";
     242              :                     else
     243            1 :                         return null;
     244            4 :                 });
     245            4 :     }
     246              : 
     247            1 :     function get_custom_ntp_timesyncd() {
     248            1 :         const custom_ntp_config_file = cockpit.file("/etc/systemd/timesyncd.conf.d/50-cockpit.conf",
     249            1 :                                                     { superuser: "try" });
     250              : 
     251            1 :         const result = {
     252            1 :             backend: "timesyncd",
     253            1 :             enabled: false,
     254            1 :             servers: []
     255            1 :         };
     256              : 
     257            1 :         return custom_ntp_config_file.read()
     258            1 :                 .then(function(text) {
     259            1 :                     let ntp_line = "";
     260            1 :                     if (text) {
     261            1 :                         result.enabled = true;
     262            1 :                         text.split("\n").forEach(function(line) {
     263            1 :                             if (line.indexOf("NTP=") === 0) {
     264            1 :                                 ntp_line = line.slice(4);
     265            1 :                                 result.enabled = true;
     266            0 :                             } else if (line.indexOf("#NTP=") === 0) {
     267            0 :                                 ntp_line = line.slice(5);
     268            0 :                                 result.enabled = false;
     269            0 :                             }
     270            1 :                         });
     271              : 
     272            1 :                         result.servers = ntp_line.split(" ").filter(function(val) {
     273            1 :                             return val !== "";
     274            1 :                         });
     275            1 :                         if (result.servers.length === 0)
     276            0 :                             result.enabled = false;
     277            1 :                     }
     278            1 :                     return result;
     279            1 :                 })
     280            0 :                 .catch(function(error) {
     281            0 :                     console.warn("failed to load time servers", error);
     282            0 :                     return result;
     283            0 :                 });
     284            1 :     }
     285              : 
     286            1 :     async function set_custom_ntp_timesyncd(config) {
     287            1 :         const conf_path = "/etc/systemd/timesyncd.conf.d/50-cockpit.conf";
     288            1 :         const custom_ntp_config_file = cockpit.file(conf_path, { superuser: "require" });
     289              : 
     290            1 :         const text = `# This file is automatically generated by Cockpit\n\n[Time]\n${config.enabled ? "" : "#"}NTP=${config.servers.join(" ")}\n`;
     291              : 
     292              :         // this must be readable with tight umask, timesyncd runs as unprivileged user
     293            1 :         await cockpit.spawn(["mkdir", "-p", "-m755", "/etc/systemd/timesyncd.conf.d"], { superuser: "require" });
     294            1 :         await custom_ntp_config_file.replace(text);
     295            1 :         await cockpit.spawn(["chmod", "644", conf_path], { superuser: "require" });
     296            1 :     }
     297              : 
     298           92 :     const chronyd_sourcedir = "/etc/chrony/sources.d";
     299           92 :     const chronyd_sources_enabled = chronyd_sourcedir + "/cockpit.sources";
     300           92 :     const chronyd_sources_disabled = chronyd_sourcedir + "/cockpit.disabled";
     301              : 
     302            2 :     function get_custom_ntp_chronyd() {
     303            2 :         const enabled_file = cockpit.file(chronyd_sources_enabled, { superuser: "try" });
     304            2 :         const disabled_file = cockpit.file(chronyd_sources_disabled, { superuser: "try" });
     305              : 
     306            2 :         function parse_servers(data) {
     307            2 :             if (!data)
     308            2 :                 return [];
     309            2 :             const servers = [];
     310            2 :             data.split("\n").forEach(function(line) {
     311            2 :                 const parts = line.split(" ");
     312            2 :                 if (parts[0] == "server")
     313            1 :                     servers.push(parts[1]);
     314            2 :             });
     315            2 :             return servers;
     316            2 :         }
     317              : 
     318            2 :         return enabled_file.read()
     319            2 :                 .then(data => {
     320            1 :                     if (data) {
     321            1 :                         return {
     322            1 :                             backend: "chronyd",
     323            1 :                             enabled: true,
     324            1 :                             servers: parse_servers(data)
     325            1 :                         };
     326            1 :                     } else {
     327            2 :                         return disabled_file.read()
     328            2 :                                 .then(data => {
     329            2 :                                     return {
     330            2 :                                         backend: "chronyd",
     331            2 :                                         enabled: false,
     332            2 :                                         servers: parse_servers(data)
     333            2 :                                     };
     334            2 :                                 });
     335            2 :                     }
     336            2 :                 });
     337            2 :     }
     338              : 
     339            2 :     function set_custom_ntp_chronyd(config) {
     340            2 :         const enabled_file = cockpit.file(chronyd_sources_enabled, { superuser: "require" });
     341            2 :         const disabled_file = cockpit.file(chronyd_sources_disabled, { superuser: "require" });
     342              : 
     343            1 :         const text = "# This file is automatically generated by Cockpit\n\n" + config.servers.map(s => `server ${s}\n`).join("");
     344              : 
     345              :         // HACK - https://bugzilla.redhat.com/show_bug.cgi?id=2168863
     346            1 :         function ensure_sourcedir() {
     347            1 :             function add_sourcedir(data) {
     348            1 :                 const line = "sourcedir " + chronyd_sourcedir;
     349            1 :                 if (data && data.indexOf(line) == -1)
     350            1 :                     data += "\n# Added by Cockpit\n" + line + "\n";
     351            1 :                 return data;
     352            1 :             }
     353            1 :             return cockpit.file("/etc/chrony.conf", { superuser: "require" }).modify(add_sourcedir);
     354            1 :         }
     355              : 
     356            2 :         return cockpit.spawn(["mkdir", "-p", chronyd_sourcedir], { superuser: "require" })
     357            2 :                 .then(() => {
     358            2 :                     if (config.enabled)
     359            1 :                         return enabled_file.replace(text).then(() => disabled_file.replace(null)).then(ensure_sourcedir);
     360              :                     else
     361            2 :                         return disabled_file.replace(text).then(() => enabled_file.replace(null));
     362            2 :                 });
     363            2 :     }
     364              : 
     365            4 :     self.get_custom_ntp = function () {
     366            4 :         return get_timesync_backend().then(backend => {
     367            1 :             if (backend == "timesyncd") {
     368            1 :                 return get_custom_ntp_timesyncd();
     369            0 :             } else if (backend == "chronyd") {
     370            2 :                 return get_custom_ntp_chronyd();
     371            0 :             } else {
     372            1 :                 return Promise.resolve({ backend: null, servers: [], enabled: false });
     373            1 :             }
     374            4 :         });
     375            4 :     };
     376              : 
     377            3 :     self.set_custom_ntp = function (config) {
     378            1 :         if (config.backend == "timesyncd") {
     379            1 :             return set_custom_ntp_timesyncd(config);
     380            0 :         } else if (config.backend == "chronyd") {
     381            2 :             return set_custom_ntp_chronyd(config);
     382            0 :         } else {
     383            0 :             return Promise.resolve();
     384            0 :         }
     385            3 :     };
     386              : 
     387            4 :     self.get_timezones = function() {
     388            4 :         return cockpit.spawn(["/usr/bin/timedatectl", "list-timezones"])
     389            4 :                 .then(content => content.split('\n').filter(tz => tz != ""));
     390            4 :     };
     391              : 
     392              :     /* NTPSynchronized needs to be polled so we just do that
     393              :      * always.
     394              :      */
     395              : 
     396           34 :     const ntp_poll_interval = window.setInterval(function() {
     397           34 :         self.poll_ntp_synchronized();
     398           34 :     }, 5000);
     399              : 
     400            0 :     self.close = function close() {
     401            0 :         window.clearInterval(updateInterval);
     402            0 :         window.clearInterval(ntp_poll_interval);
     403            0 :         client.close();
     404            0 :     };
     405              : 
     406           92 :     connect();
     407           92 :     superuser.addEventListener("reconnect", connect);
     408           92 :     self.update();
     409           92 : }
     410              : 
     411           92 : export function ServerTimeConfig() {
     412           92 :     const server_time = useObject(() => new ServerTime(),
     413            0 :                                   st => st.close(),
     414           92 :                                   []);
     415           92 :     useEvent(server_time, "changed");
     416              : 
     417           92 :     const ntp = server_time.get_ntp_status();
     418              : 
     419           92 :     const tz = server_time.get_time_zone();
     420           92 :     const systime_button = (
     421           92 :         <Button variant="link" id="system_information_systime_button"
     422            4 :                 onClick={ () => change_systime_dialog(server_time, tz) }
     423           92 :                 data-timedated-initialized={ntp?.initialized}
     424           65 :                 isInline isDisabled={!superuser.allowed || !tz}>
     425           92 :             { timeformat.dateTimeUTC(server_time.utc_fake_now) }
     426           92 :         </Button>);
     427              : 
     428           92 :     let ntp_status = null;
     429           77 :     if (ntp?.active) {
     430           77 :         let icon; let header; let body = ""; let footer = null;
     431           16 :         if (ntp.synch) {
     432           16 :             icon = <Icon status="info"><InfoCircleIcon className="ct-info-circle" /></Icon>;
     433           16 :             header = _("Synchronized");
     434           16 :             if (ntp.server)
     435           16 :                 body = <div>{cockpit.format(_("Synchronized with $0"), ntp.server)}</div>;
     436           16 :         } else {
     437           16 :             if (ntp.server) {
     438           16 :                 icon = <Spinner size="md" />;
     439           16 :                 header = _("Synchronizing");
     440           16 :                 body = <div>{cockpit.format(_("Trying to synchronize with $0"), ntp.server)}</div>;
     441           16 :             } else {
     442           77 :                 icon = <Icon status="danger"><ExclamationCircleIcon className="ct-exclamation-circle" /></Icon>;
     443           77 :                 header = _("Not synchronized");
     444           73 :                 if (ntp.service) {
     445           73 :                     footer = (
     446           73 :                         <Button variant="link"
     447            0 :                                 onClick={() => cockpit.jump("/system/services#/" +
     448            0 :                                                             encodeURIComponent(ntp.service))}>
     449           73 :                             {_("Log messages")}
     450           73 :                         </Button>);
     451           73 :                 }
     452           77 :             }
     453           77 :         }
     454              : 
     455           17 :         if (ntp.sub_status) {
     456           17 :             body = <>{body}<div>{ntp.sub_status}</div></>;
     457           17 :         }
     458              : 
     459           77 :         ntp_status = (
     460           77 :             <Popover headerContent={header} bodyContent={body} footerContent={footer}>
     461           77 :                 {icon}
     462           77 :             </Popover>);
     463           77 :     }
     464              : 
     465           92 :     return (
     466           92 :         <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
     467           92 :             {systime_button}
     468           92 :             {ntp_status}
     469           92 :         </Flex>
     470              :     );
     471           92 : }
     472              : 
     473            4 : function Validated({ errors, error_key, children }) {
     474            4 :     const error = errors?.[error_key];
     475              :     // We need to always render the <div> for the has-error
     476              :     // class so that the input field keeps the focus when
     477              :     // errors are cleared.  Otherwise the DOM changes enough
     478              :     // for the Browser to remove focus.
     479            4 :     return (
     480            1 :         <div className={error ? "ct-validation-wrapper has-error" : "ct-validation-wrapper"}>
     481            4 :             { children }
     482            1 :             { error ? <span className="help-block dialog-error">{error}</span> : null }
     483            4 :         </div>
     484              :     );
     485            4 : }
     486              : 
     487            2 : function ValidatedInput({ errors, error_key, children }) {
     488            2 :     const error = errors?.[error_key];
     489            2 :     return (
     490            1 :         <span className={error ? "ct-validation-wrapper has-error" : "ct-validation-wrapper"}>
     491            2 :             { children }
     492            2 :         </span>
     493              :     );
     494            2 : }
     495              : 
     496            4 : function ChangeSystimeBody({ state, errors, change }) {
     497            4 :     const {
     498            4 :         time_zone, time_zones,
     499            4 :         mode,
     500            4 :         manual_date, manual_time,
     501            4 :         ntp_supported, custom_ntp
     502            4 :     } = state;
     503              : 
     504            2 :     function add_server(event, index) {
     505            2 :         custom_ntp.servers.splice(index + 1, 0, "");
     506            2 :         change("custom_ntp", custom_ntp);
     507            2 :         event.stopPropagation();
     508            2 :         event.preventDefault();
     509            2 :         return false;
     510            2 :     }
     511              : 
     512            0 :     function remove_server(event, index) {
     513            0 :         custom_ntp.servers.splice(index, 1);
     514            0 :         change("custom_ntp", custom_ntp);
     515            0 :         event.stopPropagation();
     516            0 :         event.preventDefault();
     517            0 :         return false;
     518            0 :     }
     519              : 
     520            2 :     function change_server(event, index, value) {
     521            2 :         custom_ntp.servers[index] = value;
     522            2 :         change("custom_ntp", custom_ntp);
     523            2 :         event.stopPropagation();
     524            2 :         event.preventDefault();
     525            2 :         return false;
     526            2 :     }
     527              : 
     528            4 :     const ntp_servers = (
     529            4 :         custom_ntp.servers.map((s, i) => (
     530            4 :             <Flex className="ntp-server-input-group" spaceItems={{ default: 'spaceItemsSm' }} key={i}>
     531            4 :                 <FlexItem grow={{ default: 'grow' }}>
     532            4 :                     <TextInput value={s} placeholder={_("NTP server")} aria-label={_("NTP server")}
     533            2 :                                onChange={(event, value) => change_server(event, i, value)} />
     534            4 :                 </FlexItem>
     535            2 :                 <Button variant="secondary" onClick={event => add_server(event, i)}
     536            4 :                         icon={ <PlusIcon /> } />
     537            0 :                 <Button variant="secondary" onClick={event => remove_server(event, i)}
     538            4 :                         icon={ <CloseIcon /> } isDisabled={i === custom_ntp.servers.length - 1} />
     539            4 :             </Flex>
     540            4 :         ))
     541              :     );
     542              : 
     543            4 :     const mode_options = [
     544            4 :         { value: "manual_time", content: _("Manually") },
     545            4 :         { value: "ntp_time", content: _("Automatically using NTP"), isDisabled: !ntp_supported },
     546            4 :     ];
     547              : 
     548            4 :     if (custom_ntp.backend)
     549            3 :         mode_options.push(
     550            3 :             {
     551            3 :                 value: "ntp_time_custom",
     552            3 :                 isDisabled: !ntp_supported,
     553            3 :                 content: (custom_ntp.backend == "chronyd")
     554            2 :                     ? _("Automatically using additional NTP servers")
     555            1 :                     : _("Automatically using specific NTP servers")
     556            3 :             });
     557              : 
     558            4 :     return (
     559            4 :         <Form isHorizontal>
     560            4 :             <FormGroup fieldId="systime-timezones" label={_("Time zone")}>
     561            4 :                 <Validated errors={errors} error_key="time_zone">
     562            4 :                     <TypeaheadSelect toggleProps={ { id: "systime-timezones" } }
     563            4 :                                      isScrollable
     564            4 :                                      selected={time_zone}
     565            0 :                                      onSelect={(event, value) => { change("time_zone", value) }}
     566            4 :                                      selectOptions={time_zones.map(tz => (
     567            4 :                                          { value: tz, content: tz.replaceAll("_", " ") }
     568            4 :                                      ))} />
     569            4 :                 </Validated>
     570            4 :             </FormGroup>
     571            4 :             <FormGroup fieldId="change_systime_btn" label={_("Set time")} isStack>
     572            4 :                 <SimpleSelect
     573            4 :                     id="change_systime"
     574            4 :                     options={mode_options}
     575            4 :                     selected={mode}
     576            4 :                     toggleProps={{ id: "change_systime_btn" }}
     577            3 :                     onSelect={value => change("mode", value)} />
     578            4 :                 { mode == "manual_time" &&
     579            2 :                     <Flex spaceItems={{ default: 'spaceItemsSm' }} id="systime-manual-row">
     580            2 :                         <ValidatedInput errors={errors} error_key="manual_date">
     581            2 :                             <DatePicker id="systime-date-input"
     582            2 :                                         aria-label={_("Pick date")}
     583            2 :                                         buttonAriaLabel={_("Toggle date picker")}
     584            2 :                                         invalidFormatText=""
     585            2 :                                         locale={timeformat.dateFormatLang()}
     586            2 :                                         weekStart={timeformat.firstDayOfWeek()}
     587            1 :                                         onChange={(_, d) => change("manual_date", d)}
     588            2 :                                         value={manual_date}
     589            0 :                                         appendTo={() => document.body} />
     590            2 :                         </ValidatedInput>
     591            2 :                         <ValidatedInput errors={errors} error_key="manual_time">
     592            2 :                             <TimePicker id="systime-time-input"
     593            2 :                                         className="ct-serverTime-time-picker"
     594            2 :                                         time={manual_time}
     595            2 :                                         is24Hour
     596            1 :                                         menuAppendTo={() => document.body}
     597            2 :                                         invalidFormatErrorMessage=""
     598            1 :                                         onChange={(e, time, h, m, s, valid) => change("manual_time", time, valid) } />
     599            2 :                         </ValidatedInput>
     600            2 :                         <Validated errors={errors} error_key="manual_date" />
     601            2 :                         <Validated errors={errors} error_key="manual_time" />
     602            2 :                     </Flex>
     603              :                 }
     604            4 :                 { mode == "ntp_time_custom" &&
     605            2 :                     <Validated errors={errors} error_key="ntp_servers">
     606            2 :                         <div id="systime-ntp-servers">
     607            2 :                             { ntp_servers }
     608            2 :                         </div>
     609            2 :                     </Validated>
     610              :                 }
     611            4 :             </FormGroup>
     612            4 :         </Form>
     613              :     );
     614            4 : }
     615              : 
     616            3 : function has_errors(errors) {
     617            1 :     for (const field in errors) {
     618            1 :         if (errors[field])
     619            1 :             return true;
     620            1 :     }
     621            3 :     return false;
     622            3 : }
     623              : 
     624            4 : function change_systime_dialog(server_time, timezone) {
     625            4 :     let dlg = null;
     626            4 :     const state = {
     627            4 :         time_zone: timezone,
     628            4 :         time_zones: null,
     629            4 :         mode: null,
     630            4 :         ntp_supported: server_time.get_ntp_supported(),
     631            4 :         custom_ntp: null,
     632            4 :         manual_time_valid: true,
     633            4 :     };
     634            4 :     let errors = { };
     635              : 
     636            2 :     function get_current_time() {
     637            2 :         state.manual_date = server_time.utc_fake_now.toISOString().split("T")[0];
     638              : 
     639            2 :         const minutes = server_time.utc_fake_now.getUTCMinutes();
     640              :         // normalize to two digits
     641            1 :         const minutes_str = (minutes < 10) ? "0" + minutes.toString() : minutes.toString();
     642            2 :         state.manual_time = `${server_time.utc_fake_now.getUTCHours()}:${minutes_str}`;
     643            2 :     }
     644              : 
     645            3 :     function change(field, value, isValid) {
     646            3 :         state[field] = value;
     647            3 :         errors = { };
     648              : 
     649            3 :         if (field == "mode" && value == "manual_time")
     650            1 :             get_current_time();
     651              : 
     652            3 :         if (field == "manual_time")
     653            1 :             state.manual_time_valid = value && isValid;
     654              : 
     655            3 :         update();
     656            3 :     }
     657              : 
     658            3 :     function validate() {
     659            3 :         errors = { };
     660              : 
     661            3 :         if (state.time_zone == "")
     662            0 :             errors.time_zone = _("Invalid timezone");
     663              : 
     664            1 :         if (state.mode == "manual_time") {
     665            1 :             const new_date = new Date(state.manual_date);
     666            1 :             if (isNaN(new_date.getTime()) || new_date.getTime() < 0)
     667            0 :                 errors.manual_date = _("Invalid date format");
     668              : 
     669            1 :             if (!state.manual_time_valid)
     670            1 :                 errors.manual_time = _("Invalid time format");
     671            1 :         }
     672              : 
     673            2 :         if (state.mode == "ntp_time_custom") {
     674            2 :             if (state.custom_ntp.servers.filter(s => !!s).length == 0)
     675            0 :                 errors.ntp_servers = _("Need at least one NTP server");
     676            2 :         }
     677              : 
     678            3 :         return !has_errors(errors);
     679            3 :     }
     680              : 
     681            3 :     function apply() {
     682            3 :         return server_time.set_time_zone(state.time_zone)
     683            3 :                 .then(() => {
     684            1 :                     if (state.mode == "manual_time") {
     685            1 :                         return server_time.set_ntp(false)
     686            1 :                                 .then(() => server_time.change_time(state.manual_date,
     687            1 :                                                                     state.manual_time));
     688            1 :                     } else {
     689              :                         // Switch off NTP, write the config file, and switch NTP back on
     690            3 :                         state.custom_ntp.enabled = (state.mode == "ntp_time_custom");
     691            3 :                         state.custom_ntp.servers = state.custom_ntp.servers.filter(s => !!s);
     692            3 :                         return server_time.set_ntp(false)
     693            3 :                                 .then(() => server_time.set_custom_ntp(state.custom_ntp))
     694            3 :                                 .then(() => server_time.set_ntp(true));
     695            3 :                     }
     696            3 :                 });
     697            3 :     }
     698              : 
     699            4 :     function update() {
     700            4 :         const props = {
     701            4 :             id: "system_information_change_systime",
     702            4 :             title: _("Change system time"),
     703            4 :             body: <ChangeSystimeBody state={state} errors={errors} change={change} />
     704            4 :         };
     705              : 
     706            4 :         const footer = {
     707            4 :             actions: [
     708            4 :                 {
     709            4 :                     caption: _("Change"),
     710            4 :                     style: "primary",
     711            3 :                     clicked: () => {
     712            3 :                         if (validate()) {
     713            3 :                             return apply();
     714            1 :                         } else {
     715            1 :                             update();
     716            1 :                             return Promise.reject();
     717            1 :                         }
     718            3 :                     }
     719            4 :                 }
     720            4 :             ]
     721            4 :         };
     722              : 
     723            4 :         if (!dlg)
     724            3 :             dlg = show_modal_dialog(props, footer);
     725            3 :         else {
     726            3 :             dlg.setProps(props);
     727            3 :             dlg.setFooterProps(footer);
     728            3 :         }
     729            4 :     }
     730              : 
     731            4 :     Promise.all([server_time.get_custom_ntp(), server_time.get_timezones()])
     732            4 :             .then(([custom_ntp, time_zones]) => {
     733            4 :                 if (custom_ntp.servers.length == 0)
     734            4 :                     custom_ntp.servers = [""];
     735            4 :                 state.custom_ntp = custom_ntp;
     736            4 :                 state.time_zones = time_zones;
     737            3 :                 if (server_time.get_ntp_active()) {
     738            3 :                     if (custom_ntp.enabled)
     739            2 :                         state.mode = "ntp_time_custom";
     740              :                     else
     741            3 :                         state.mode = "ntp_time";
     742            1 :                 } else {
     743            2 :                     state.mode = "manual_time";
     744            2 :                     get_current_time();
     745            2 :                 }
     746            4 :                 update();
     747            4 :             });
     748            4 : }
        

Generated by: LCOV version 2.0-1