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