Line data Source code
1 : // SPDX-License-Identifier: LGPL-2.1-or-later
2 123 : import cockpit from "cockpit";
3 : import { import_Manifests } from "../manifests";
4 : import { validate } from "import-json";
5 : import { host_superuser_storage_key } from "superuser-dialogs";
6 :
7 : import ssh_add_key_sh from "../../lib/ssh-add-key.sh";
8 :
9 123 : const mod = { };
10 :
11 : /*
12 : * We share the Machines state between multiple frames. Only
13 : * one frame has the job of loading the state, usually index.js
14 : * The Loader code below does all the loading.
15 : *
16 : * The data is stored in sessionStorage in a JSON object, like this
17 : * {
18 : * content: name → info dict from bridge's /machines Machines property
19 : * overlay: extra data to augment and override on top of content
20 : * }
21 : *
22 : * This uses sessionStorage rather than cockpit.sessionStorage
23 : * because we don't ever want to write unprefixed keys.
24 : */
25 :
26 123 : const key = cockpit.sessionStorage.prefixedKey("v2-machines.json");
27 123 : const session_prefix = cockpit.sessionStorage.prefixedKey("v1-session-machine");
28 :
29 0 : function generate_session_key(host) {
30 0 : return session_prefix + "/" + host;
31 0 : }
32 :
33 120 : export function get_init_superuser_for_options(options) {
34 120 : let value = null;
35 120 : const key = host_superuser_storage_key(options.host);
36 120 : if (key)
37 119 : value = window.localStorage.getItem(key);
38 :
39 : /* When connecting, we can optionally try to start a privileged
40 : * bridge immediately. However, it is quite likely that that
41 : * needs a password and if we don't have one, it will likely fail.
42 : * That would be okay, but sudo is very noisy about failures and
43 : * might send nasty emails to your parents. For that reason we
44 : * pass "init-superuser": "none" here when there is no password.
45 : *
46 : * The downside is that if sudo is configured to not require a
47 : * password, we could start it successfully immediately as part
48 : * of the connection process, which would be convenient. However,
49 : * if sudo works without password, gaining admin privs is just a
50 : * single click, and the convenience loss is not that of a big deal,
51 : * hopefully.
52 : */
53 :
54 19 : if (value == "sudo" && !options.password)
55 19 : value = "none";
56 :
57 120 : return value;
58 120 : }
59 :
60 120 : export function generate_connection_string(user, port, addr) {
61 120 : let address = addr;
62 120 : if (user)
63 19 : address = user + "@" + address;
64 :
65 120 : if (port)
66 19 : address = address + ":" + port;
67 :
68 120 : return address;
69 120 : }
70 :
71 131 : export function split_connection_string (conn_to) {
72 131 : const parts = { address: "" };
73 131 : let user_spot = -1;
74 131 : let port_spot = -1;
75 :
76 131 : if (conn_to) {
77 131 : if (conn_to.substring(0, 6) === "ssh://")
78 30 : conn_to = conn_to.substring(6);
79 131 : user_spot = conn_to.lastIndexOf('@');
80 131 : port_spot = conn_to.lastIndexOf(':');
81 131 : }
82 :
83 30 : if (user_spot > 0) {
84 30 : parts.user = conn_to.substring(0, user_spot);
85 30 : conn_to = conn_to.substring(user_spot + 1);
86 30 : port_spot = conn_to.lastIndexOf(':');
87 30 : }
88 :
89 30 : if (port_spot > -1) {
90 30 : const port = parseInt(conn_to.substring(port_spot + 1), 10);
91 30 : if (!isNaN(port)) {
92 30 : parts.port = port;
93 30 : conn_to = conn_to.substring(0, port_spot);
94 30 : }
95 30 : }
96 :
97 131 : parts.address = conn_to;
98 131 : return parts;
99 131 : }
100 :
101 123 : function import_manifests(val) {
102 123 : return validate("manifests", val, import_Manifests, {});
103 123 : }
104 :
105 123 : function Machines() {
106 123 : const self = this;
107 :
108 123 : cockpit.event_target(self);
109 :
110 123 : let flat = null;
111 123 : self.ready = false;
112 :
113 : /* parsed machine data */
114 123 : const machines = { };
115 :
116 : /* Data shared between Machines() instances */
117 123 : let last = {
118 123 : content: null,
119 123 : overlay: {
120 123 : localhost: {
121 123 : visible: true,
122 123 : manifests: import_manifests(cockpit.manifests)
123 123 : }
124 123 : }
125 123 : };
126 :
127 23 : function storage(ev) {
128 1 : if (ev.key === key && ev.storageArea === window.sessionStorage)
129 1 : refresh(JSON.parse(ev.newValue || "null"));
130 23 : }
131 :
132 123 : window.addEventListener("storage", storage);
133 :
134 123 : window.setTimeout(function() {
135 123 : const value = window.sessionStorage.getItem(key);
136 123 : if (!self.ready && value)
137 19 : refresh(JSON.parse(value));
138 123 : });
139 :
140 123 : let timeout = null;
141 :
142 120 : function sync(machine, values, overlay) {
143 120 : const desired = { ...values, ...overlay };
144 120 : for (const prop in desired) {
145 120 : if (machine[prop] !== desired[prop])
146 120 : machine[prop] = desired[prop];
147 120 : }
148 120 : for (const prop in machine) {
149 120 : if (machine[prop] !== desired[prop])
150 120 : delete machine[prop];
151 120 : }
152 120 : return machine;
153 120 : }
154 :
155 120 : function refresh(shared, push) {
156 120 : if (!shared)
157 120 : return;
158 :
159 120 : last = shared;
160 120 : flat = null;
161 :
162 120 : if (push && !timeout) {
163 120 : timeout = window.setTimeout(function() {
164 120 : timeout = null;
165 120 : window.sessionStorage.setItem(key, JSON.stringify(last));
166 120 : }, 10);
167 120 : }
168 :
169 120 : const hosts = { };
170 19 : const content = shared.content || { };
171 19 : const overlay = shared.overlay || { };
172 120 : for (const host in content)
173 19 : hosts[host] = true;
174 120 : for (const host in overlay)
175 120 : hosts[host] = true;
176 :
177 120 : const events = [];
178 :
179 120 : for (const host in hosts) {
180 120 : const old_machine = machines[host] || { };
181 120 : const old_conns = old_machine.connection_string;
182 :
183 : /* Invert logic for color, always respect what's on disk */
184 19 : if (content[host] && content[host].color && overlay[host])
185 19 : delete overlay[host].color;
186 :
187 120 : const machine = sync(old_machine, content[host], overlay[host]);
188 :
189 : /* Fill in defaults */
190 120 : machine.key = host;
191 120 : if (!machine.address)
192 120 : machine.address = host;
193 :
194 120 : machine.connection_string = generate_connection_string(machine.user,
195 120 : machine.port,
196 120 : machine.address);
197 :
198 120 : if (!machine.label) {
199 19 : if (host == "localhost" || host == "localhost.localdomain") {
200 120 : const application = cockpit.transport.application();
201 120 : if (application.indexOf('cockpit+=') === 0)
202 19 : machine.label = application.replace('cockpit+=', '');
203 : else
204 120 : machine.label = window.location.hostname;
205 19 : } else {
206 19 : machine.label = host;
207 19 : }
208 120 : }
209 120 : if (!machine.avatar)
210 120 : machine.avatar = "../shell/images/server-small.png";
211 :
212 120 : events.push([host in machines ? "updated" : "added",
213 120 : [machine, host, old_conns]]);
214 120 : machines[host] = machine;
215 120 : }
216 :
217 : /* Remove any lost hosts */
218 120 : for (const host in machines) {
219 19 : if (!(host in hosts)) {
220 19 : const machine = machines[host];
221 19 : delete machines[host];
222 19 : delete overlay[host];
223 19 : events.push(["removed", [machine, host]]);
224 19 : }
225 120 : }
226 :
227 : /* Fire off all events */
228 120 : const len = events.length;
229 120 : for (let i = 0; i < len; i++) {
230 120 : self.dispatchEvent(events[i][0], ...events[i][1]);
231 120 : }
232 120 : }
233 :
234 0 : function update_session_machine(machine, host, values) {
235 : /* We don't save the whole machine object */
236 0 : const skey = generate_session_key(host);
237 0 : const data = { ...machine, ...values };
238 0 : window.sessionStorage.setItem(skey, JSON.stringify(data));
239 0 : self.overlay(host, values);
240 0 : return Promise.resolve([]);
241 0 : }
242 :
243 0 : function update_saved_machine(host, values) {
244 : // wrap values in variants for D-Bus call; at least values.port can
245 : // be int or string, so stringify everything but the "visible" boolean
246 0 : const values_variant = {};
247 0 : for (const prop in values) {
248 0 : if (values[prop] !== null) {
249 0 : if (prop == "visible")
250 0 : values_variant[prop] = cockpit.variant('b', values[prop]);
251 : else
252 0 : values_variant[prop] = cockpit.variant('s', values[prop].toString());
253 0 : }
254 0 : }
255 :
256 : // FIXME: investigate reusing the proxy from Loader (runs in different frame/scope)
257 0 : const bridge = cockpit.dbus(null, { bus: "internal", superuser: "require" });
258 0 : const mod =
259 0 : bridge.call("/machines", "cockpit.Machines", "Update", ["99-webui.json", host, values_variant])
260 0 : .catch(error => {
261 : // avoid make noise when we are not superuser
262 0 : if (error.problem !== "access-denied")
263 0 : console.error("failed to call cockpit.Machines.Update(): ", JSON.stringify(error));
264 0 : })
265 0 : .then(() => self.overlay(host, values));
266 :
267 0 : return mod;
268 0 : }
269 :
270 120 : self.set_ready = function ready() {
271 120 : if (!self.ready) {
272 120 : self.ready = true;
273 120 : self.dispatchEvent("ready");
274 120 : }
275 120 : };
276 :
277 0 : self.add_key = function(host_key) {
278 0 : return cockpit.script(ssh_add_key_sh, [host_key.trim(), "known_hosts"], { err: "message" });
279 0 : };
280 :
281 0 : self.add = function add(connection_string, color) {
282 0 : let values = split_connection_string(connection_string);
283 0 : const host = values.address;
284 :
285 0 : values = {
286 0 : visible: true,
287 0 : color: color || self.unused_color(),
288 0 : ...values
289 0 : };
290 :
291 0 : const machine = self.lookup(host);
292 0 : if (machine)
293 0 : machine.on_disk = true;
294 :
295 0 : return self.change(values.address, values);
296 0 : };
297 :
298 112 : self.unused_color = function unused_color() {
299 112 : const len = mod.colors.length;
300 112 : for (let i = 0; i < len; i++) {
301 112 : if (!color_in_use(mod.colors[i]))
302 112 : return mod.colors[i];
303 112 : }
304 11 : return "gray";
305 112 : };
306 :
307 112 : function color_in_use(color) {
308 112 : const norm = mod.colors.parse(color);
309 112 : for (const key in machines) {
310 112 : const machine = machines[key];
311 11 : if (machine.color && mod.colors.parse(machine.color) == norm)
312 11 : return true;
313 112 : }
314 112 : return false;
315 112 : }
316 :
317 120 : function merge(item, values) {
318 120 : for (const prop in values) {
319 120 : if (values[prop] === null)
320 120 : delete item[prop];
321 : else
322 120 : item[prop] = values[prop];
323 120 : }
324 120 : }
325 :
326 0 : self.change = function change(host, values) {
327 0 : const machine = self.lookup(host);
328 :
329 0 : if (machine && !machine.on_disk)
330 0 : return update_session_machine(machine, host, values);
331 : else
332 0 : return update_saved_machine(host, values);
333 0 : };
334 :
335 120 : self.data = function data(content) {
336 120 : const changes = {};
337 :
338 19 : for (const host in content) {
339 19 : changes[host] = { ...last.overlay[host] };
340 19 : merge(changes[host], { on_disk: true });
341 19 : }
342 :
343 : /* It's a full reload, so data not
344 : * present is no longer from disk
345 : */
346 19 : for (const host in machines) {
347 19 : if (content && !content[host]) {
348 19 : changes[host] = { ...last.overlay[host] };
349 19 : merge(changes[host], { on_disk: null });
350 19 : }
351 19 : }
352 :
353 120 : refresh({
354 120 : content,
355 120 : overlay: { ...last.overlay, ...changes },
356 120 : }, true);
357 120 : };
358 :
359 120 : self.overlay = function overlay(host, values) {
360 120 : const address = split_connection_string(host).address;
361 120 : const changes = { };
362 120 : changes[address] = { ...last.overlay[address] };
363 120 : merge(changes[address], values);
364 120 : refresh({
365 120 : content: last.content,
366 120 : overlay: { ...last.overlay, ...changes }
367 120 : }, true);
368 120 : };
369 :
370 123 : Object.defineProperty(self, "list", {
371 123 : enumerable: true,
372 0 : get: function get() {
373 0 : if (!flat) {
374 0 : flat = [];
375 0 : for (const key in machines) {
376 0 : if (machines[key].visible)
377 0 : flat.push(machines[key]);
378 0 : }
379 0 : flat.sort(function(m1, m2) {
380 0 : return m1.label.localeCompare(m2.label);
381 0 : });
382 0 : }
383 0 : return flat;
384 0 : }
385 123 : });
386 :
387 123 : Object.defineProperty(self, "addresses", {
388 123 : enumerable: true,
389 0 : get: function get() {
390 0 : return Object.keys(machines);
391 0 : }
392 123 : });
393 :
394 131 : self.lookup = function lookup(address) {
395 131 : const parts = split_connection_string(address);
396 30 : return machines[parts.address || "localhost"] || null;
397 131 : };
398 :
399 0 : self.close = function close() {
400 0 : window.removeEventListener("storage", storage);
401 0 : };
402 123 : }
403 :
404 123 : function Loader(machines, session_only) {
405 123 : const self = this;
406 :
407 : /* Have we loaded from cockpit session */
408 123 : let session_loaded = false;
409 :
410 : /* echo channels to each machine */
411 123 : const channels = { };
412 123 : const channels_listeners_message = { };
413 123 : const channels_listeners_close = { };
414 :
415 : /* hostnamed proxies to each machine, if hostnamed available */
416 123 : const proxies = { };
417 123 : const proxies_listeners_changed = { };
418 :
419 : /* clients for the bridge D-Bus API */
420 123 : const bridge_dbus = { };
421 :
422 119 : function process_session_key(key, value) {
423 119 : const parts = key.split("/");
424 119 : if (parts[0] == session_prefix &&
425 19 : parts.length === 2) {
426 19 : const host = parts[1];
427 19 : if (value) {
428 19 : const values = JSON.parse(value);
429 19 : const machine = machines.lookup(host);
430 19 : if (!machine || !machine.on_disk)
431 19 : machines.overlay(host, values);
432 19 : else if (!machine.visible)
433 19 : machines.change(host, { visible: true });
434 19 : self.connect(host);
435 19 : }
436 19 : }
437 119 : }
438 :
439 120 : function load_from_session_storage() {
440 120 : session_loaded = true;
441 119 : for (let i = 0; i < window.sessionStorage.length; i++) {
442 119 : const k = window.sessionStorage.key(i);
443 119 : process_session_key(k, window.sessionStorage.getItem(k));
444 119 : }
445 120 : }
446 :
447 23 : function process_session_machines(ev) {
448 23 : if (ev.storageArea === window.sessionStorage)
449 1 : process_session_key(ev.key || "", ev.newValue);
450 23 : }
451 123 : window.addEventListener("storage", process_session_machines);
452 :
453 120 : function state(host, value, problem) {
454 120 : const values = { state: value, problem };
455 120 : if (value == "connected") {
456 120 : values.restarting = false;
457 19 : } else if (problem) {
458 19 : values.manifests = null;
459 19 : values.checksum = null;
460 19 : if (problem == "authentication-failed" || problem == "authentication-not-supported")
461 19 : values.restarting = false;
462 19 : }
463 120 : machines.overlay(host, values);
464 120 : }
465 :
466 123 : machines.addEventListener("added", updated);
467 123 : machines.addEventListener("updated", updated);
468 123 : machines.addEventListener("removed", removed);
469 :
470 120 : function updated(ev, machine, host, old_conns) {
471 120 : if (!machine) {
472 120 : machine = machines.lookup(host);
473 120 : if (!machine)
474 120 : return;
475 120 : }
476 :
477 120 : let props = proxies[host];
478 120 : if (!props || !props.valid)
479 120 : props = { };
480 :
481 120 : const overlay = { };
482 :
483 120 : if (!machine.color)
484 120 : overlay.color = machines.unused_color();
485 :
486 120 : const label = props.PrettyHostname || props.StaticHostname || props.Hostname;
487 120 : if (label && label !== machine.label)
488 120 : overlay.label = label;
489 :
490 120 : const os = props.OperatingSystemPrettyName;
491 120 : if (os && os != machine.os)
492 120 : overlay.os = props.OperatingSystemPrettyName;
493 :
494 120 : if (Object.keys(overlay).length > 0)
495 120 : machines.overlay(host, overlay);
496 :
497 : /* Don't automatically reconnect failed machines, and don't
498 : * automatically connect to new machines. The navigation will
499 : * explicitly connect as necessary.
500 : */
501 120 : if (machine.visible) {
502 19 : if (old_conns && machine.connection_string != old_conns) {
503 19 : cockpit.kill(old_conns);
504 19 : self.disconnect(host);
505 19 : self.connect(host);
506 19 : }
507 19 : } else {
508 19 : self.disconnect(host);
509 19 : }
510 120 : }
511 :
512 0 : function removed(ev, machine, host) {
513 0 : self.disconnect(host);
514 0 : }
515 :
516 120 : self.connect = function connect(host) {
517 120 : const machine = machines.lookup(host);
518 120 : if (!machine)
519 120 : return;
520 :
521 120 : let channel = channels[host];
522 120 : if (channel)
523 120 : return;
524 :
525 120 : const options = {
526 120 : host: machine.connection_string,
527 120 : payload: "echo",
528 120 : };
529 :
530 120 : options["init-superuser"] = get_init_superuser_for_options(options);
531 :
532 19 : if (!machine.on_disk && machine.host_key) {
533 19 : options['temp-session'] = false; /* Compatibility option */
534 19 : options.session = 'shared';
535 19 : options['host-key'] = machine.host_key;
536 19 : }
537 :
538 120 : channel = cockpit.channel(options);
539 120 : channels[host] = channel;
540 :
541 120 : const local = host === "localhost";
542 :
543 : /* Request is null, and message is true when connected */
544 120 : let request = null;
545 120 : let open = local;
546 :
547 120 : let url;
548 19 : if (!machine.manifests) {
549 19 : if (machine.checksum)
550 19 : url = "../../" + machine.checksum + "/manifests.json";
551 : else
552 19 : url = "../../@" + encodeURI(machine.connection_string) + "/manifests.json";
553 19 : }
554 :
555 120 : function whirl() {
556 120 : if (!request && open)
557 19 : state(host, "connected", null);
558 : else
559 19 : state(host, "connecting", null);
560 120 : }
561 :
562 : /* Here we load the machine manifests, and expect them before going to "connected" */
563 0 : function request_manifest() {
564 0 : request = new XMLHttpRequest();
565 0 : request.responseType = "json";
566 0 : request.open("GET", url, true);
567 0 : request.addEventListener("load", () => {
568 0 : const overlay = { manifests: import_manifests(request.response) };
569 0 : const etag = request.getResponseHeader("ETag");
570 0 : if (etag) /* and remove quotes */
571 0 : overlay.checksum = etag.replace(/^"(.+)"$/, '$1');
572 0 : machines.overlay(host, overlay);
573 :
574 0 : request = null;
575 0 : whirl();
576 0 : });
577 0 : request.addEventListener("error", () => {
578 0 : console.warn("failed to load manifests from " + machine.connection_string);
579 0 : request = null;
580 0 : whirl();
581 0 : });
582 0 : request.send();
583 0 : }
584 :
585 : /* Try to get change notifications via the internal
586 : /packages D-Bus interface of the bridge. Not all
587 : bridges support this API, so we still get the first
588 : version of the manifests via HTTP in request_manifest.
589 : */
590 :
591 120 : function watch_manifests() {
592 120 : const dbus = cockpit.dbus(null, {
593 120 : bus: "internal",
594 120 : host: machine.connection_string
595 120 : });
596 120 : bridge_dbus[host] = dbus;
597 120 : dbus.subscribe({
598 120 : path: "/packages",
599 120 : interface: "org.freedesktop.DBus.Properties",
600 120 : member: "PropertiesChanged"
601 120 : },
602 11 : function (path, iface, member, args) {
603 11 : if (args[0] == "cockpit.Packages") {
604 11 : if (args[1].Manifests) {
605 11 : const manifests = JSON.parse(args[1].Manifests.v);
606 11 : machines.overlay(host, { manifests: import_manifests(manifests) });
607 11 : }
608 11 : }
609 11 : });
610 :
611 : /* Tell the bridge to reload the packages, but only if
612 : it hasn't just started. Thus, nothing happens on
613 : the first login, but if you reload the shell, we
614 : will also reload the packages.
615 : */
616 120 : dbus.call("/packages", "cockpit.Packages", "ReloadHint", []);
617 120 : }
618 :
619 120 : function request_hostname() {
620 120 : if (!machine.static_hostname) {
621 120 : const proxy = cockpit.dbus("org.freedesktop.hostname1",
622 120 : { host: machine.connection_string }).proxy();
623 120 : proxies[host] = proxy;
624 120 : proxy.wait(function() {
625 0 : proxies_listeners_changed[host] = () => updated(null, null, host);
626 120 : proxy.addEventListener("changed", proxies_listeners_changed[host]);
627 120 : updated(null, null, host);
628 120 : });
629 120 : }
630 120 : }
631 :
632 : /* Send a message to the server and get back a message once connected */
633 19 : if (!local) {
634 19 : channel.send("x");
635 :
636 0 : channels_listeners_message[host] = () => {
637 0 : open = true;
638 0 : if (url)
639 0 : request_manifest();
640 0 : watch_manifests();
641 0 : request_hostname();
642 0 : whirl();
643 0 : };
644 19 : channel.addEventListener("message", channels_listeners_message[host]);
645 :
646 0 : channels_listeners_close[host] = (ev, options) => {
647 0 : const m = machines.lookup(host);
648 0 : open = false;
649 : // reset to clean state when removing machine (orderly disconnect), otherwise mark as failed
650 0 : if (!options.problem && m && !m.visible)
651 0 : state(host, null, null);
652 : else
653 0 : state(host, "failed", options.problem || "disconnected");
654 0 : if (m && m.restarting) {
655 0 : window.setTimeout(function() {
656 0 : self.connect(host);
657 0 : }, 10000);
658 0 : }
659 0 : self.disconnect(host);
660 0 : };
661 19 : channel.addEventListener("close", channels_listeners_close[host]);
662 19 : } else {
663 120 : if (url)
664 19 : request_manifest();
665 120 : watch_manifests();
666 120 : request_hostname();
667 120 : }
668 :
669 : /* In case already ready, for example when local */
670 120 : whirl();
671 120 : };
672 :
673 0 : self.disconnect = function disconnect(host) {
674 0 : if (host === "localhost")
675 0 : return;
676 :
677 0 : const channel = channels[host];
678 0 : delete channels[host];
679 0 : if (channel) {
680 0 : channel.close();
681 0 : channel.removeEventListener("message", channels_listeners_message[host]);
682 0 : channel.removeEventListener("close", channels_listeners_close[host]);
683 0 : }
684 :
685 0 : const proxy = proxies[host];
686 0 : delete proxies[host];
687 0 : if (proxy) {
688 0 : proxy.client.close();
689 0 : proxy.removeEventListener("changed", proxies_listeners_changed[host]);
690 0 : }
691 :
692 0 : const dbus = bridge_dbus[host];
693 0 : delete bridge_dbus[host];
694 0 : if (dbus) {
695 0 : dbus.close();
696 0 : }
697 0 : };
698 :
699 0 : self.expect_restart = function expect_restart(host) {
700 0 : const parts = split_connection_string(host);
701 0 : machines.overlay(parts.address, {
702 0 : restarting: true,
703 0 : problem: null
704 0 : });
705 0 : };
706 :
707 0 : self.close = function close() {
708 0 : machines.removeEventListener("added", updated);
709 0 : machines.removeEventListener("changed", updated);
710 0 : machines.removeEventListener("removed", removed);
711 0 : machines = null;
712 :
713 0 : window.removeEventListener("storage", process_session_machines);
714 0 : const hosts = Object.keys(channels);
715 0 : hosts.forEach(self.disconnect);
716 0 : };
717 :
718 123 : if (!session_only) {
719 123 : const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Machines", "/machines");
720 120 : proxy.addEventListener("changed", data => {
721 : // unwrap variants from D-Bus call
722 120 : const wrapped = proxy.Machines;
723 120 : cockpit.assert(typeof wrapped === "object" && wrapped !== null, "unexpected type of Machines property");
724 120 : const data_unwrap = {};
725 19 : for (const host in wrapped) {
726 19 : const host_props = {};
727 19 : for (const prop in wrapped[host])
728 19 : host_props[prop] = wrapped[host][prop].v;
729 19 : data_unwrap[host] = host_props;
730 19 : }
731 :
732 120 : machines.data(data_unwrap);
733 120 : if (!session_loaded)
734 120 : load_from_session_storage();
735 120 : machines.set_ready();
736 120 : });
737 19 : } else {
738 19 : load_from_session_storage();
739 19 : machines.data({});
740 19 : machines.set_ready();
741 19 : }
742 123 : }
743 :
744 123 : mod.instance = function instance(loader) {
745 123 : return new Machines();
746 123 : };
747 :
748 123 : mod.loader = function loader(machines, session_only) {
749 123 : return new Loader(machines, session_only);
750 123 : };
751 :
752 123 : mod.colors = [
753 123 : "#0099d3",
754 123 : "#67d300",
755 123 : "#d39e00",
756 123 : "#d3007c",
757 123 : "#00d39f",
758 123 : "#00d1d3",
759 123 : "#00618a",
760 123 : "#4c8a00",
761 123 : "#8a6600",
762 123 : "#9b005b",
763 123 : "#008a55",
764 123 : "#008a8a",
765 123 : "#00b9ff",
766 123 : "#7dff00",
767 123 : "#ffbe00",
768 123 : "#ff0096",
769 123 : "#00ffc0",
770 123 : "#00fdff",
771 123 : "#023448",
772 123 : "#264802",
773 123 : "#483602",
774 123 : "#590034",
775 123 : "#024830",
776 123 : "#024848"
777 123 : ];
778 :
779 112 : mod.colors.parse = function parse_color(input) {
780 112 : const div = document.createElement('div');
781 112 : div.style.color = input;
782 112 : const style = window.getComputedStyle(div, null);
783 112 : return style.getPropertyValue("color") || div.style.color;
784 112 : };
785 :
786 123 : export const machines = mod;
|