Line data Source code
1 : /*
2 : * Copyright (C) 2019 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 114 : import cockpit from "cockpit";
6 114 : 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 114 : const _ = cockpit.gettext;
32 :
33 95 : export function ServerTime() {
34 95 : const self = this;
35 95 : cockpit.event_target(self);
36 :
37 87 : function emit_changed() {
38 87 : self.dispatchEvent("changed");
39 87 : }
40 :
41 95 : let time_offset = null;
42 95 : let remote_offset = null;
43 :
44 95 : let client = null;
45 95 : let timedate = null;
46 :
47 95 : function connect() {
48 25 : if (client) {
49 25 : timedate.removeEventListener("changed", emit_changed);
50 25 : client.close();
51 25 : }
52 95 : client = cockpit.dbus('org.freedesktop.timedate1', { superuser: "try" });
53 95 : timedate = client.proxy();
54 95 : timedate.addEventListener("changed", emit_changed);
55 95 : client.subscribe({
56 95 : interface: "org.freedesktop.DBus.Properties",
57 95 : member: "PropertiesChanged"
58 95 : }, ntp_updated);
59 95 : }
60 :
61 95 : const timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
62 95 : const timesyncd_service = service.proxy("systemd-timesyncd.service");
63 95 : const chronyd_service = service.proxy("chronyd.service");
64 :
65 95 : timesyncd_service.addEventListener("changed", emit_changed);
66 95 : 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 95 : Object.defineProperty(self, 'utc_fake_now', {
74 95 : enumerable: true,
75 95 : get: function get() {
76 95 : const offset = time_offset + remote_offset;
77 95 : return new Date(offset + (new Date()).valueOf());
78 95 : }
79 95 : });
80 :
81 95 : Object.defineProperty(self, 'now', {
82 95 : enumerable: true,
83 1 : get: function get() {
84 1 : return new Date(time_offset + (new Date()).valueOf());
85 1 : }
86 95 : });
87 :
88 95 : 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 95 : self.update = function update() {
97 95 : return cockpit.spawn(["date", "+%s:%z"], { err: "message" })
98 86 : .then(data => {
99 86 : const parts = data.trim().split(":");
100 86 : const timems = parseInt(parts[0], 10) * 1000;
101 86 : let tzmin = parseInt(parts[1].slice(-2), 10);
102 86 : const tzhour = parseInt(parts[1].slice(0, -2));
103 86 : if (tzhour < 0)
104 17 : tzmin = -tzmin;
105 86 : const offsetms = (tzhour * 3600000) + tzmin * 60000;
106 86 : const now = new Date();
107 86 : time_offset = (timems - now.valueOf());
108 86 : remote_offset = offsetms;
109 86 : emit_changed();
110 86 : })
111 0 : .catch(ex => console.log("Couldn't calculate server time offset: " + cockpit.message(ex)));
112 95 : };
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 95 : self.get_time_zone = function () {
128 95 : return timedate.Timezone;
129 95 : };
130 :
131 3 : self.set_time_zone = function (tz) {
132 3 : return timedate.call('SetTimezone', [tz, true]);
133 3 : };
134 :
135 37 : self.poll_ntp_synchronized = () => client.call(
136 37 : timedate.path, "org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTPSynchronized"])
137 34 : .then(result => {
138 34 : const ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
139 34 : const data = { };
140 34 : data[timedate.path] = ifaces;
141 34 : client.notify(data);
142 34 : })
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 95 : let ntp_waiting_value = null;
150 95 : 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 95 : self.get_ntp_status = function () {
194 95 : const status = {
195 95 : initialized: false,
196 95 : active: false,
197 95 : synch: false,
198 95 : service: null,
199 95 : server: null,
200 95 : sub_status: null
201 95 : };
202 :
203 : // flag for tests that timedated/timesyncd proxies got initialized
204 95 : if (timedate.CanNTP !== undefined &&
205 76 : timedate1_service.unit && timedate1_service.unit.Id &&
206 76 : timesyncd_service.exists !== null &&
207 76 : chronyd_service.exists !== null)
208 75 : status.initialized = true;
209 :
210 95 : status.active = timedate.NTP;
211 95 : status.synch = timedate.NTPSynchronized;
212 :
213 95 : const timesyncd_server_regex = /.*time server (.*)\./i;
214 :
215 95 : const timesyncd_status = (timesyncd_service.state == "running" &&
216 18 : timesyncd_service.service?.StatusText);
217 :
218 95 : if (timesyncd_service.state == "running")
219 18 : status.service = "systemd-timesyncd.service";
220 95 : else if (chronyd_service.state == "running")
221 76 : status.service = "chronyd.service";
222 :
223 18 : if (timesyncd_status) {
224 18 : const match = timesyncd_status.match(timesyncd_server_regex);
225 18 : if (match)
226 17 : status.server = match[1];
227 18 : else if (timesyncd_status != "Idle." && timesyncd_status !== "")
228 18 : status.sub_status = timesyncd_status;
229 18 : }
230 :
231 95 : return status;
232 95 : };
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 95 : const chronyd_sourcedir = "/etc/chrony/sources.d";
299 95 : const chronyd_sources_enabled = chronyd_sourcedir + "/cockpit.sources";
300 95 : 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 37 : const ntp_poll_interval = window.setInterval(function() {
397 37 : self.poll_ntp_synchronized();
398 37 : }, 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 95 : connect();
407 95 : superuser.addEventListener("reconnect", connect);
408 95 : self.update();
409 95 : }
410 :
411 95 : export function ServerTimeConfig() {
412 95 : const server_time = useObject(() => new ServerTime(),
413 0 : st => st.close(),
414 95 : []);
415 95 : useEvent(server_time, "changed");
416 :
417 95 : const ntp = server_time.get_ntp_status();
418 :
419 95 : const tz = server_time.get_time_zone();
420 95 : const systime_button = (
421 95 : <Button variant="link" id="system_information_systime_button"
422 4 : onClick={ () => change_systime_dialog(server_time, tz) }
423 95 : data-timedated-initialized={ntp?.initialized}
424 66 : isInline isDisabled={!superuser.allowed || !tz}>
425 95 : { timeformat.dateTimeUTC(server_time.utc_fake_now) }
426 95 : </Button>);
427 :
428 95 : let ntp_status = null;
429 80 : if (ntp?.active) {
430 80 : let icon; let header; let body = ""; let footer = null;
431 17 : if (ntp.synch) {
432 17 : icon = <Icon status="info"><InfoCircleIcon className="ct-info-circle" /></Icon>;
433 17 : header = _("Synchronized");
434 17 : if (ntp.server)
435 17 : body = <div>{cockpit.format(_("Synchronized with $0"), ntp.server)}</div>;
436 17 : } else {
437 17 : if (ntp.server) {
438 17 : icon = <Spinner size="md" />;
439 17 : header = _("Synchronizing");
440 17 : body = <div>{cockpit.format(_("Trying to synchronize with $0"), ntp.server)}</div>;
441 17 : } else {
442 80 : icon = <Icon status="danger"><ExclamationCircleIcon className="ct-exclamation-circle" /></Icon>;
443 80 : header = _("Not synchronized");
444 74 : if (ntp.service) {
445 74 : footer = (
446 74 : <Button variant="link"
447 0 : onClick={() => cockpit.jump("/system/services#/" +
448 0 : encodeURIComponent(ntp.service))}>
449 74 : {_("Log messages")}
450 74 : </Button>);
451 74 : }
452 80 : }
453 80 : }
454 :
455 18 : if (ntp.sub_status) {
456 18 : body = <>{body}<div>{ntp.sub_status}</div></>;
457 18 : }
458 :
459 80 : ntp_status = (
460 80 : <Popover headerContent={header} bodyContent={body} footerContent={footer}>
461 80 : {icon}
462 80 : </Popover>);
463 80 : }
464 :
465 95 : return (
466 95 : <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
467 95 : {systime_button}
468 95 : {ntp_status}
469 95 : </Flex>
470 : );
471 95 : }
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 0 : 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 : }
|