Line data Source code
1 : // SPDX-License-Identifier: LGPL-2.1-or-later
2 339 : 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 339 : 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 339 : const key = cockpit.sessionStorage.prefixedKey("v2-machines.json");
27 339 : 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 336 : export function get_init_superuser_for_options(options) {
34 336 : let value = null;
35 336 : const key = host_superuser_storage_key(options.host);
36 336 : if (key)
37 335 : 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 62 : if (value == "sudo" && !options.password)
55 62 : value = "none";
56 :
57 336 : return value;
58 336 : }
59 :
60 336 : export function generate_connection_string(user, port, addr) {
61 336 : let address = addr;
62 336 : if (user)
63 64 : address = user + "@" + address;
64 :
65 336 : if (port)
66 63 : address = address + ":" + port;
67 :
68 336 : return address;
69 336 : }
70 :
71 363 : export function split_connection_string (conn_to) {
72 363 : const parts = { address: "" };
73 363 : let user_spot = -1;
74 363 : let port_spot = -1;
75 :
76 363 : if (conn_to) {
77 363 : if (conn_to.substring(0, 6) === "ssh://")
78 89 : conn_to = conn_to.substring(6);
79 363 : user_spot = conn_to.lastIndexOf('@');
80 363 : port_spot = conn_to.lastIndexOf(':');
81 363 : }
82 :
83 91 : if (user_spot > 0) {
84 91 : parts.user = conn_to.substring(0, user_spot);
85 91 : conn_to = conn_to.substring(user_spot + 1);
86 91 : port_spot = conn_to.lastIndexOf(':');
87 91 : }
88 :
89 90 : if (port_spot > -1) {
90 90 : const port = parseInt(conn_to.substring(port_spot + 1), 10);
91 90 : if (!isNaN(port)) {
92 90 : parts.port = port;
93 90 : conn_to = conn_to.substring(0, port_spot);
94 90 : }
95 90 : }
96 :
97 363 : parts.address = conn_to;
98 363 : return parts;
99 363 : }
100 :
101 339 : function import_manifests(val) {
102 339 : return validate("manifests", val, import_Manifests, {});
103 339 : }
104 :
105 339 : function Machines() {
106 339 : const self = this;
107 :
108 339 : cockpit.event_target(self);
109 :
110 339 : let flat = null;
111 339 : self.ready = false;
112 :
113 : /* parsed machine data */
114 339 : const machines = { };
115 :
116 : /* Data shared between Machines() instances */
117 339 : let last = {
118 339 : content: null,
119 339 : overlay: {
120 339 : localhost: {
121 339 : visible: true,
122 339 : manifests: import_manifests(cockpit.manifests)
123 339 : }
124 339 : }
125 339 : };
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 339 : window.addEventListener("storage", storage);
133 :
134 339 : window.setTimeout(function() {
135 339 : const value = window.sessionStorage.getItem(key);
136 339 : if (!self.ready && value)
137 62 : refresh(JSON.parse(value));
138 339 : });
139 :
140 339 : let timeout = null;
141 :
142 336 : function sync(machine, values, overlay) {
143 336 : const desired = { ...values, ...overlay };
144 336 : for (const prop in desired) {
145 336 : if (machine[prop] !== desired[prop])
146 336 : machine[prop] = desired[prop];
147 336 : }
148 336 : for (const prop in machine) {
149 336 : if (machine[prop] !== desired[prop])
150 336 : delete machine[prop];
151 336 : }
152 336 : return machine;
153 336 : }
154 :
155 336 : function refresh(shared, push) {
156 336 : if (!shared)
157 336 : return;
158 :
159 336 : last = shared;
160 336 : flat = null;
161 :
162 336 : if (push && !timeout) {
163 336 : timeout = window.setTimeout(function() {
164 336 : timeout = null;
165 336 : window.sessionStorage.setItem(key, JSON.stringify(last));
166 336 : }, 10);
167 336 : }
168 :
169 336 : const hosts = { };
170 62 : const content = shared.content || { };
171 62 : const overlay = shared.overlay || { };
172 336 : for (const host in content)
173 68 : hosts[host] = true;
174 336 : for (const host in overlay)
175 336 : hosts[host] = true;
176 :
177 336 : const events = [];
178 :
179 336 : for (const host in hosts) {
180 336 : const old_machine = machines[host] || { };
181 336 : const old_conns = old_machine.connection_string;
182 :
183 : /* Invert logic for color, always respect what's on disk */
184 68 : if (content[host] && content[host].color && overlay[host])
185 68 : delete overlay[host].color;
186 :
187 336 : const machine = sync(old_machine, content[host], overlay[host]);
188 :
189 : /* Fill in defaults */
190 336 : machine.key = host;
191 336 : if (!machine.address)
192 336 : machine.address = host;
193 :
194 336 : machine.connection_string = generate_connection_string(machine.user,
195 336 : machine.port,
196 336 : machine.address);
197 :
198 336 : if (!machine.label) {
199 71 : if (host == "localhost" || host == "localhost.localdomain") {
200 336 : const application = cockpit.transport.application();
201 336 : if (application.indexOf('cockpit+=') === 0)
202 62 : machine.label = application.replace('cockpit+=', '');
203 : else
204 336 : machine.label = window.location.hostname;
205 71 : } else {
206 71 : machine.label = host;
207 71 : }
208 336 : }
209 336 : if (!machine.avatar)
210 336 : machine.avatar = "../shell/images/server-small.png";
211 :
212 336 : events.push([host in machines ? "updated" : "added",
213 336 : [machine, host, old_conns]]);
214 336 : machines[host] = machine;
215 336 : }
216 :
217 : /* Remove any lost hosts */
218 336 : for (const host in machines) {
219 62 : if (!(host in hosts)) {
220 62 : const machine = machines[host];
221 62 : delete machines[host];
222 62 : delete overlay[host];
223 62 : events.push(["removed", [machine, host]]);
224 62 : }
225 336 : }
226 :
227 : /* Fire off all events */
228 336 : const len = events.length;
229 336 : for (let i = 0; i < len; i++) {
230 336 : self.dispatchEvent(events[i][0], ...events[i][1]);
231 336 : }
232 336 : }
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 336 : self.set_ready = function ready() {
271 336 : if (!self.ready) {
272 336 : self.ready = true;
273 336 : self.dispatchEvent("ready");
274 336 : }
275 336 : };
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 336 : function merge(item, values) {
318 336 : for (const prop in values) {
319 336 : if (values[prop] === null)
320 336 : delete item[prop];
321 : else
322 336 : item[prop] = values[prop];
323 336 : }
324 336 : }
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 336 : self.data = function data(content) {
336 336 : const changes = {};
337 :
338 68 : for (const host in content) {
339 68 : changes[host] = { ...last.overlay[host] };
340 68 : merge(changes[host], { on_disk: true });
341 68 : }
342 :
343 : /* It's a full reload, so data not
344 : * present is no longer from disk
345 : */
346 67 : for (const host in machines) {
347 67 : if (content && !content[host]) {
348 67 : changes[host] = { ...last.overlay[host] };
349 67 : merge(changes[host], { on_disk: null });
350 67 : }
351 67 : }
352 :
353 336 : refresh({
354 336 : content,
355 336 : overlay: { ...last.overlay, ...changes },
356 336 : }, true);
357 336 : };
358 :
359 336 : self.overlay = function overlay(host, values) {
360 336 : const address = split_connection_string(host).address;
361 336 : const changes = { };
362 336 : changes[address] = { ...last.overlay[address] };
363 336 : merge(changes[address], values);
364 336 : refresh({
365 336 : content: last.content,
366 336 : overlay: { ...last.overlay, ...changes }
367 336 : }, true);
368 336 : };
369 :
370 339 : Object.defineProperty(self, "list", {
371 339 : 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 339 : });
386 :
387 339 : Object.defineProperty(self, "addresses", {
388 339 : enumerable: true,
389 12 : get: function get() {
390 12 : return Object.keys(machines);
391 12 : }
392 339 : });
393 :
394 363 : self.lookup = function lookup(address) {
395 363 : const parts = split_connection_string(address);
396 96 : return machines[parts.address || "localhost"] || null;
397 363 : };
398 :
399 0 : self.close = function close() {
400 0 : window.removeEventListener("storage", storage);
401 0 : };
402 339 : }
403 :
404 339 : function Loader(machines, session_only) {
405 339 : const self = this;
406 :
407 : /* Have we loaded from cockpit session */
408 339 : let session_loaded = false;
409 :
410 : /* echo channels to each machine */
411 339 : const channels = { };
412 339 : const channels_listeners_message = { };
413 339 : const channels_listeners_close = { };
414 :
415 : /* hostnamed proxies to each machine, if hostnamed available */
416 339 : const proxies = { };
417 339 : const proxies_listeners_changed = { };
418 :
419 : /* clients for the bridge D-Bus API */
420 339 : const bridge_dbus = { };
421 :
422 334 : function process_session_key(key, value) {
423 334 : const parts = key.split("/");
424 334 : if (parts[0] == session_prefix &&
425 62 : parts.length === 2) {
426 62 : const host = parts[1];
427 62 : if (value) {
428 62 : const values = JSON.parse(value);
429 62 : const machine = machines.lookup(host);
430 62 : if (!machine || !machine.on_disk)
431 62 : machines.overlay(host, values);
432 62 : else if (!machine.visible)
433 62 : machines.change(host, { visible: true });
434 62 : self.connect(host);
435 62 : }
436 62 : }
437 334 : }
438 :
439 336 : function load_from_session_storage() {
440 336 : session_loaded = true;
441 334 : for (let i = 0; i < window.sessionStorage.length; i++) {
442 334 : const k = window.sessionStorage.key(i);
443 334 : process_session_key(k, window.sessionStorage.getItem(k));
444 334 : }
445 336 : }
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 339 : window.addEventListener("storage", process_session_machines);
452 :
453 336 : function state(host, value, problem) {
454 336 : const values = { state: value, problem };
455 336 : if (value == "connected") {
456 336 : values.restarting = false;
457 65 : } else if (problem) {
458 65 : values.manifests = null;
459 65 : values.checksum = null;
460 65 : if (problem == "authentication-failed" || problem == "authentication-not-supported")
461 62 : values.restarting = false;
462 65 : }
463 336 : machines.overlay(host, values);
464 336 : }
465 :
466 339 : machines.addEventListener("added", updated);
467 339 : machines.addEventListener("updated", updated);
468 339 : machines.addEventListener("removed", removed);
469 :
470 336 : function updated(ev, machine, host, old_conns) {
471 336 : if (!machine) {
472 336 : machine = machines.lookup(host);
473 336 : if (!machine)
474 336 : return;
475 336 : }
476 :
477 336 : let props = proxies[host];
478 336 : if (!props || !props.valid)
479 336 : props = { };
480 :
481 336 : const overlay = { };
482 :
483 336 : if (!machine.color)
484 336 : overlay.color = machines.unused_color();
485 :
486 336 : const label = props.PrettyHostname || props.StaticHostname || props.Hostname;
487 336 : if (label && label !== machine.label)
488 336 : overlay.label = label;
489 :
490 336 : const os = props.OperatingSystemPrettyName;
491 336 : if (os && os != machine.os)
492 336 : overlay.os = props.OperatingSystemPrettyName;
493 :
494 336 : if (Object.keys(overlay).length > 0)
495 336 : 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 336 : if (machine.visible) {
502 64 : if (old_conns && machine.connection_string != old_conns) {
503 64 : cockpit.kill(old_conns);
504 64 : self.disconnect(host);
505 64 : self.connect(host);
506 64 : }
507 64 : } else {
508 64 : self.disconnect(host);
509 64 : }
510 336 : }
511 :
512 0 : function removed(ev, machine, host) {
513 0 : self.disconnect(host);
514 0 : }
515 :
516 336 : self.connect = function connect(host) {
517 336 : const machine = machines.lookup(host);
518 336 : if (!machine)
519 336 : return;
520 :
521 336 : let channel = channels[host];
522 336 : if (channel)
523 336 : return;
524 :
525 336 : const options = {
526 336 : host: machine.connection_string,
527 336 : payload: "echo",
528 336 : };
529 :
530 336 : options["init-superuser"] = get_init_superuser_for_options(options);
531 :
532 62 : if (!machine.on_disk && machine.host_key) {
533 62 : options['temp-session'] = false; /* Compatibility option */
534 62 : options.session = 'shared';
535 62 : options['host-key'] = machine.host_key;
536 62 : }
537 :
538 336 : channel = cockpit.channel(options);
539 336 : channels[host] = channel;
540 :
541 336 : const local = host === "localhost";
542 :
543 : /* Request is null, and message is true when connected */
544 336 : let request = null;
545 336 : let open = local;
546 :
547 336 : let url;
548 71 : if (!machine.manifests) {
549 71 : if (machine.checksum)
550 62 : url = "../../" + machine.checksum + "/manifests.json";
551 : else
552 71 : url = "../../@" + encodeURI(machine.connection_string) + "/manifests.json";
553 71 : }
554 :
555 336 : function whirl() {
556 336 : if (!request && open)
557 71 : state(host, "connected", null);
558 : else
559 71 : state(host, "connecting", null);
560 336 : }
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 336 : function watch_manifests() {
592 336 : const dbus = cockpit.dbus(null, {
593 336 : bus: "internal",
594 336 : host: machine.connection_string
595 336 : });
596 336 : bridge_dbus[host] = dbus;
597 336 : dbus.subscribe({
598 336 : path: "/packages",
599 336 : interface: "org.freedesktop.DBus.Properties",
600 336 : member: "PropertiesChanged"
601 336 : },
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 336 : dbus.call("/packages", "cockpit.Packages", "ReloadHint", []);
617 336 : }
618 :
619 336 : function request_hostname() {
620 336 : if (!machine.static_hostname) {
621 336 : const proxy = cockpit.dbus("org.freedesktop.hostname1",
622 336 : { host: machine.connection_string }).proxy();
623 336 : proxies[host] = proxy;
624 336 : proxy.wait(function() {
625 0 : proxies_listeners_changed[host] = () => updated(null, null, host);
626 336 : proxy.addEventListener("changed", proxies_listeners_changed[host]);
627 336 : updated(null, null, host);
628 336 : });
629 336 : }
630 336 : }
631 :
632 : /* Send a message to the server and get back a message once connected */
633 71 : if (!local) {
634 71 : 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 71 : 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 71 : channel.addEventListener("close", channels_listeners_close[host]);
662 71 : } else {
663 336 : if (url)
664 62 : request_manifest();
665 336 : watch_manifests();
666 336 : request_hostname();
667 336 : }
668 :
669 : /* In case already ready, for example when local */
670 336 : whirl();
671 336 : };
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 339 : if (!session_only) {
719 339 : const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Machines", "/machines");
720 336 : proxy.addEventListener("changed", data => {
721 : // unwrap variants from D-Bus call
722 336 : const wrapped = proxy.Machines;
723 336 : cockpit.assert(typeof wrapped === "object" && wrapped !== null, "unexpected type of Machines property");
724 336 : const data_unwrap = {};
725 68 : for (const host in wrapped) {
726 68 : const host_props = {};
727 68 : for (const prop in wrapped[host])
728 68 : host_props[prop] = wrapped[host][prop].v;
729 68 : data_unwrap[host] = host_props;
730 68 : }
731 :
732 336 : machines.data(data_unwrap);
733 336 : if (!session_loaded)
734 336 : load_from_session_storage();
735 336 : machines.set_ready();
736 336 : });
737 62 : } else {
738 62 : load_from_session_storage();
739 62 : machines.data({});
740 62 : machines.set_ready();
741 62 : }
742 339 : }
743 :
744 339 : mod.instance = function instance(loader) {
745 339 : return new Machines();
746 339 : };
747 :
748 339 : mod.loader = function loader(machines, session_only) {
749 339 : return new Loader(machines, session_only);
750 339 : };
751 :
752 339 : mod.colors = [
753 339 : "#0099d3",
754 339 : "#67d300",
755 339 : "#d39e00",
756 339 : "#d3007c",
757 339 : "#00d39f",
758 339 : "#00d1d3",
759 339 : "#00618a",
760 339 : "#4c8a00",
761 339 : "#8a6600",
762 339 : "#9b005b",
763 339 : "#008a55",
764 339 : "#008a8a",
765 339 : "#00b9ff",
766 339 : "#7dff00",
767 339 : "#ffbe00",
768 339 : "#ff0096",
769 339 : "#00ffc0",
770 339 : "#00fdff",
771 339 : "#023448",
772 339 : "#264802",
773 339 : "#483602",
774 339 : "#590034",
775 339 : "#024830",
776 339 : "#024848"
777 339 : ];
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 339 : export const machines = mod;
|