606 lines
21 KiB
JavaScript
606 lines
21 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2016 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import cockpit from "cockpit";
|
|
import React from "react";
|
|
import ReactDOM from "react-dom";
|
|
|
|
import { CockpitNav, CockpitNavItem, SidebarToggle } from "./nav.jsx";
|
|
import { TopNav } from ".//topnav.jsx";
|
|
import { CockpitHosts } from "./hosts.jsx";
|
|
import { codes, HostModal } from "./hosts_dialog.jsx";
|
|
import { EarlyFailure, EarlyFailureReady } from './failures.jsx';
|
|
import { WithDialogs } from "dialogs.jsx";
|
|
|
|
import * as base_index from "./base_index";
|
|
|
|
const _ = cockpit.gettext;
|
|
|
|
function MachinesIndex(index_options, machines, loader) {
|
|
if (!index_options)
|
|
index_options = {};
|
|
|
|
const page_status = { };
|
|
sessionStorage.removeItem("cockpit:page_status");
|
|
|
|
index_options.navigate = function (state, sidebar) {
|
|
return navigate(state, sidebar);
|
|
};
|
|
index_options.handle_notifications = function (host, page, data) {
|
|
if (data.page_status !== undefined) {
|
|
if (!page_status[host])
|
|
page_status[host] = { };
|
|
page_status[host][page] = data.page_status;
|
|
sessionStorage.setItem("cockpit:page_status", JSON.stringify(page_status));
|
|
// Just for triggering an "updated" event
|
|
machines.overlay(host, { });
|
|
}
|
|
};
|
|
|
|
const index = base_index.new_index_from_proto(index_options);
|
|
|
|
/* Restarts */
|
|
index.addEventListener("expect_restart", (ev, host) => loader.expect_restart(host));
|
|
|
|
/* Disconnection Dialog */
|
|
let watchdog_problem = null;
|
|
index.addEventListener("disconnect", (ev, problem) => {
|
|
watchdog_problem = problem;
|
|
show_disconnected();
|
|
});
|
|
|
|
index.addEventListener("update", () => {
|
|
update_topbar();
|
|
});
|
|
|
|
/* Is troubleshooting dialog open */
|
|
let troubleshooting_opened = false;
|
|
|
|
ReactDOM.render(<SidebarToggle />, document.getElementById('sidebar-toggle'));
|
|
|
|
// Focus with skiplinks
|
|
const skiplinks = document.getElementsByClassName("skiplink");
|
|
Array.from(skiplinks).forEach(skiplink => {
|
|
skiplink.addEventListener("click", ev => {
|
|
document.getElementById(ev.target.hash.substring(1)).focus();
|
|
return false;
|
|
});
|
|
});
|
|
|
|
let current_user = "";
|
|
cockpit.user().then(user => {
|
|
current_user = user.name || "";
|
|
});
|
|
|
|
/* Navigation */
|
|
let ready = false;
|
|
function on_ready() {
|
|
ready = true;
|
|
index.ready();
|
|
}
|
|
|
|
function preload_frames () {
|
|
for (const m of machines.list)
|
|
index.preload_frames(m, m.manifests);
|
|
}
|
|
|
|
/* When the machine list is ready we start processing navigation */
|
|
machines.addEventListener("ready", on_ready);
|
|
machines.addEventListener("removed", (ev, machine) => {
|
|
index.frames.remove(machine);
|
|
update_machines();
|
|
});
|
|
["added", "updated"].forEach(evn => {
|
|
machines.addEventListener(evn, (ev, machine) => {
|
|
if (!machine.visible)
|
|
index.frames.remove(machine);
|
|
else if (machine.problem)
|
|
index.frames.remove(machine);
|
|
|
|
update_machines();
|
|
preload_frames();
|
|
if (ready)
|
|
navigate();
|
|
});
|
|
});
|
|
|
|
if (machines.ready)
|
|
on_ready();
|
|
|
|
function show_disconnected() {
|
|
if (!ready) {
|
|
document.getElementById("early-failure-ready").setAttribute("hidden", "hidden");
|
|
document.getElementById("early-failure").removeAttribute("hidden");
|
|
|
|
const ca_cert_url = window.sessionStorage.getItem("CACertUrl");
|
|
ReactDOM.render(<EarlyFailure ca_cert_url={
|
|
(window.navigator.userAgent.indexOf("Safari") >= 0 && ca_cert_url) ? ca_cert_url : undefined
|
|
} />, document.getElementById('early-failure'));
|
|
document.getElementById("main").setAttribute("hidden", "hidden");
|
|
document.body.removeAttribute("hidden");
|
|
return;
|
|
}
|
|
|
|
const current_frame = index.current_frame();
|
|
|
|
if (current_frame)
|
|
current_frame.setAttribute("hidden", "hidden");
|
|
|
|
document.getElementById("early-failure").setAttribute("hidden", "hidden");
|
|
document.getElementById("early-failure-ready").removeAttribute("hidden");
|
|
|
|
ReactDOM.render(
|
|
<EarlyFailureReady title={_("Disconnected")}
|
|
reconnect
|
|
watchdog_problem={watchdog_problem}
|
|
navigate={navigate}
|
|
paragraph={cockpit.message(watchdog_problem)} />, document.getElementById('early-failure-ready'));
|
|
}
|
|
|
|
/* Handles navigation */
|
|
function navigate(state, reconnect) {
|
|
/* If this is a watchdog problem or we are troubleshooting
|
|
* let the dialog handle it */
|
|
if (watchdog_problem || troubleshooting_opened)
|
|
return;
|
|
|
|
if (!state)
|
|
state = index.retrieve_state();
|
|
let machine = machines.lookup(state.host);
|
|
|
|
/* No such machine */
|
|
if (!machine) {
|
|
machine = {
|
|
key: state.host,
|
|
address: state.host,
|
|
label: state.host,
|
|
state: "failed",
|
|
problem: "not-found",
|
|
};
|
|
|
|
/* Asked to reconnect to the machine */
|
|
} else if (!machine.visible) {
|
|
machine.state = "failed";
|
|
machine.problem = "not-found";
|
|
} else if (reconnect) {
|
|
loader.connect(state.host);
|
|
}
|
|
|
|
const compiled = compile(machine);
|
|
if (machine.manifests && !state.component)
|
|
state.component = choose_component(state, compiled);
|
|
|
|
update_navbar(machine, state, compiled);
|
|
update_topbar(machine, state, compiled);
|
|
update_frame(machine, state, compiled);
|
|
|
|
/* Just replace the state, and URL */
|
|
index.jump(state, true);
|
|
}
|
|
|
|
function choose_component(state, compiled) {
|
|
/* Go for the first item */
|
|
const menu_items = compiled.ordered("menu");
|
|
if (menu_items.length > 0 && menu_items[0])
|
|
return menu_items[0].path;
|
|
|
|
return "system";
|
|
}
|
|
|
|
function update_topbar(machine, state, compiled) {
|
|
if (!state)
|
|
state = index.retrieve_state();
|
|
|
|
if (!machine)
|
|
machine = machines.lookup(state.host);
|
|
|
|
if (!compiled)
|
|
compiled = compile(machine);
|
|
|
|
ReactDOM.render(
|
|
<WithDialogs>
|
|
<TopNav index={index} state={state} machine={machine} compiled={compiled} />
|
|
</WithDialogs>,
|
|
document.getElementById("topnav"));
|
|
}
|
|
|
|
function update_navbar(machine, state, compiled) {
|
|
if (!state)
|
|
state = index.retrieve_state();
|
|
|
|
if (!machine)
|
|
machine = machines.lookup(state.host);
|
|
|
|
if (!machine || machine.state != "connected") {
|
|
ReactDOM.unmountComponentAtNode(document.getElementById("host-apps"));
|
|
return;
|
|
}
|
|
|
|
if (!compiled)
|
|
compiled = compile(machine);
|
|
|
|
if (machine.address !== "localhost") {
|
|
document.getElementById("main").style.setProperty('--ct-color-host-accent', machine.color);
|
|
} else {
|
|
// Remove property to fall back to default accent color
|
|
document.getElementById("main").style.removeProperty('--ct-color-host-accent');
|
|
}
|
|
|
|
const component_manifest = find_component(state, compiled);
|
|
|
|
// Filtering of navigation by term
|
|
function keyword_filter(item, term) {
|
|
function keyword_relevance(current_best, item) {
|
|
const translate = item.translate || false;
|
|
const weight = item.weight || 0;
|
|
let score;
|
|
let _m = "";
|
|
let best = { score: -1 };
|
|
item.matches.forEach(m => {
|
|
if (translate)
|
|
_m = _(m);
|
|
score = -1;
|
|
// Best score when starts in translate language
|
|
if (translate && _m.indexOf(term) == 0)
|
|
score = 4 + weight;
|
|
// Second best score when starts in English
|
|
else if (m.indexOf(term) == 0)
|
|
score = 3 + weight;
|
|
// Substring consider only when at least 3 letters were used
|
|
else if (term.length >= 3) {
|
|
if (translate && _m.indexOf(term) >= 0)
|
|
score = 2 + weight;
|
|
else if (m.indexOf(term) >= 0)
|
|
score = 1 + weight;
|
|
}
|
|
if (score > best.score) {
|
|
best = { keyword: m, score: score };
|
|
}
|
|
});
|
|
if (best.score > current_best.score) {
|
|
current_best = { keyword: best.keyword, score: best.score, goto: item.goto || null };
|
|
}
|
|
return current_best;
|
|
}
|
|
|
|
const new_item = Object.assign({}, item);
|
|
new_item.keyword = { score: -1 };
|
|
if (!term)
|
|
return new_item;
|
|
const best_keyword = new_item.keywords.reduce(keyword_relevance, { score: -1 });
|
|
if (best_keyword.score > -1) {
|
|
new_item.keyword = best_keyword;
|
|
return new_item;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Rendering of separate navigation menu items
|
|
function nav_item(component, term) {
|
|
const active = component_manifest === component.path;
|
|
|
|
// Parse path
|
|
let path = component.path;
|
|
let hash = component.hash;
|
|
if (component.keyword.goto) {
|
|
if (component.keyword.goto[0] === "/")
|
|
path = component.keyword.goto.substr(1);
|
|
else
|
|
hash = component.keyword.goto;
|
|
}
|
|
|
|
// Parse page status
|
|
let status = null;
|
|
if (page_status[machine.key])
|
|
status = page_status[machine.key][component.path];
|
|
|
|
return React.createElement(CockpitNavItem, {
|
|
key: component.label,
|
|
name: component.label,
|
|
active: active,
|
|
status: status,
|
|
keyword: component.keyword.keyword,
|
|
term: term,
|
|
to: index.href({ host: machine.address, component: path, hash: hash }),
|
|
jump: index.jump,
|
|
});
|
|
}
|
|
|
|
const groups = [
|
|
{
|
|
name: _("Apps"),
|
|
items: compiled.ordered("dashboard"),
|
|
}, {
|
|
name: _("System"),
|
|
items: compiled.ordered("menu"),
|
|
}, {
|
|
name: _("Tools"),
|
|
items: compiled.ordered("tools"),
|
|
}
|
|
].filter(i => i.items.length > 0);
|
|
|
|
if (compiled.items.apps && groups.length === 3)
|
|
groups[0].action = { label: _("Edit"), path: index.href({ host: machine.address, component: compiled.items.apps.path }) };
|
|
|
|
ReactDOM.render(
|
|
React.createElement(CockpitNav, {
|
|
groups: groups,
|
|
selector: "host-apps",
|
|
item_render: nav_item,
|
|
filtering: keyword_filter,
|
|
sorting: (a, b) => { return b.keyword.score - a.keyword.score },
|
|
current: state.component,
|
|
jump: index.jump,
|
|
}),
|
|
document.getElementById("host-apps"));
|
|
|
|
update_machines(state, machine);
|
|
}
|
|
|
|
function update_machines(state, machine) {
|
|
if (!state)
|
|
state = index.retrieve_state();
|
|
|
|
if (!machine)
|
|
machine = machines.lookup(state.host);
|
|
|
|
ReactDOM.render(
|
|
React.createElement(CockpitHosts, {
|
|
machine: machine || {},
|
|
machines: machines,
|
|
selector: "nav-hosts",
|
|
hostAddr: index.href,
|
|
jump: index.jump,
|
|
}),
|
|
document.getElementById("hosts-sel"));
|
|
}
|
|
|
|
function update_title(label, machine) {
|
|
if (label)
|
|
label += " - ";
|
|
else
|
|
label = "";
|
|
let suffix = index.default_title;
|
|
|
|
if (machine) {
|
|
if (machine.address === "localhost") {
|
|
const compiled = compile(machine);
|
|
if (compiled.ordered("menu").length || compiled.ordered("tools").length)
|
|
suffix = (machine.user || current_user) + "@" + machine.label;
|
|
} else {
|
|
suffix = (machine.user || current_user) + "@" + machine.label;
|
|
}
|
|
}
|
|
|
|
document.title = label + suffix;
|
|
}
|
|
|
|
function find_component(state, compiled) {
|
|
let component = state.component;
|
|
// If `state.component` is not known to any manifest, find where it comes from
|
|
if (compiled.items[state.component] === undefined) {
|
|
let s = state.component;
|
|
while (s && compiled.items[s] === undefined)
|
|
s = s.substring(0, s.lastIndexOf("/"));
|
|
component = s;
|
|
}
|
|
|
|
// Still don't know where it comes from, check for parent
|
|
if (!component) {
|
|
const comp = cockpit.manifests[state.component];
|
|
if (comp && comp.parent)
|
|
return comp.parent.component;
|
|
}
|
|
|
|
return component;
|
|
}
|
|
|
|
function update_frame(machine, state, compiled) {
|
|
function render_troubleshoot() {
|
|
troubleshooting_opened = true;
|
|
const template = codes[machine.problem] || "change-port";
|
|
ReactDOM.render(React.createElement(HostModal, {
|
|
template: template,
|
|
address: machine.address,
|
|
machines_ins: machines,
|
|
onClose: () => {
|
|
ReactDOM.unmountComponentAtNode(document.getElementById('troubleshoot-dialog'));
|
|
troubleshooting_opened = false;
|
|
navigate(null, true);
|
|
}
|
|
}),
|
|
document.getElementById('troubleshoot-dialog'));
|
|
}
|
|
|
|
let current_frame = index.current_frame();
|
|
|
|
if (machine.state != "connected") {
|
|
if (current_frame)
|
|
current_frame.setAttribute("hidden", "hidden");
|
|
current_frame = null;
|
|
index.current_frame(current_frame);
|
|
|
|
const connecting = (machine.state == "connecting");
|
|
let title, message;
|
|
if (machine.restarting) {
|
|
title = _("The machine is rebooting");
|
|
message = "";
|
|
} else if (connecting) {
|
|
title = _("Connecting to the machine");
|
|
message = "";
|
|
} else {
|
|
title = _("Not connected to host");
|
|
if (machine.problem == "not-found") {
|
|
message = _("Cannot connect to an unknown host");
|
|
} else {
|
|
const error = machine.problem || machine.state;
|
|
if (error)
|
|
message = cockpit.message(error);
|
|
else
|
|
message = "";
|
|
}
|
|
}
|
|
|
|
let troubleshooting = false;
|
|
|
|
if (!machine.restarting && (machine.problem === "no-host" || !!codes[machine.problem])) {
|
|
troubleshooting = true;
|
|
}
|
|
|
|
const restarting = !!machine.restarting;
|
|
const reconnect = !connecting && machine.problem != "not-found" && !troubleshooting;
|
|
|
|
document.querySelector("#early-failure-ready").removeAttribute("hidden");
|
|
ReactDOM.render(
|
|
<EarlyFailureReady loading={connecting || restarting}
|
|
title={title}
|
|
reconnect={reconnect}
|
|
troubleshoot={troubleshooting}
|
|
onTroubleshoot={render_troubleshoot}
|
|
watchdog_problem={watchdog_problem}
|
|
navigate={navigate}
|
|
paragraph={message} />,
|
|
document.getElementById('early-failure-ready')
|
|
);
|
|
|
|
update_title(null, machine);
|
|
|
|
/* Fall through when connecting, and allow frame to load at same time */
|
|
if (!connecting)
|
|
return;
|
|
}
|
|
|
|
let hash = state.hash;
|
|
let component = state.component;
|
|
|
|
/* Old cockpit packages, used to be in shell/shell.html */
|
|
if (machine && compiled.compat) {
|
|
const compat = compiled.compat[component];
|
|
if (compat) {
|
|
component = "shell/shell";
|
|
hash = compat;
|
|
}
|
|
}
|
|
|
|
const frame = component ? index.frames.lookup(machine, component, hash) : undefined;
|
|
if (frame != current_frame) {
|
|
if (current_frame) {
|
|
current_frame.style.display = "none";
|
|
// Reset 'data-active' only on the same host
|
|
if (frame.getAttribute('data-host') === current_frame.getAttribute('data-host'))
|
|
current_frame.setAttribute('data-active', 'false');
|
|
}
|
|
index.current_frame(frame);
|
|
}
|
|
|
|
if (machine.state == "connected") {
|
|
document.querySelector("#early-failure-ready").setAttribute("hidden", "hidden");
|
|
frame.style.display = "block";
|
|
frame.setAttribute('data-active', 'true');
|
|
frame.removeAttribute("hidden");
|
|
|
|
const component_manifest = find_component(state, compiled);
|
|
const item = compiled.items[component_manifest];
|
|
const label = item ? item.label : "";
|
|
update_title(label, machine);
|
|
if (label)
|
|
frame.setAttribute('title', label);
|
|
}
|
|
}
|
|
|
|
function compatibility(machine) {
|
|
if (!machine.manifests || machine.address === "localhost")
|
|
return null;
|
|
|
|
const shell = machine.manifests.shell || { };
|
|
const menu = shell.menu || { };
|
|
const tools = shell.tools || { };
|
|
|
|
const mapping = { };
|
|
|
|
/* The following were included in shell/shell.html in old versions */
|
|
if ("_host_" in menu)
|
|
mapping["system/host"] = "/server";
|
|
if ("_init_" in menu)
|
|
mapping["system/init"] = "/services";
|
|
if ("_network_" in menu)
|
|
mapping["network/interfaces"] = "/networking";
|
|
if ("_storage_" in menu)
|
|
mapping["storage/devices"] = "/storage";
|
|
if ("_users_" in tools)
|
|
mapping["users/local"] = "/accounts";
|
|
|
|
/* For Docker we have to guess ... some heuristics */
|
|
if ("_storage_" in menu || "_init_" in menu)
|
|
mapping["docker/containers"] = "/containers";
|
|
|
|
return mapping;
|
|
}
|
|
|
|
function compile(machine) {
|
|
const compiled = base_index.new_compiled();
|
|
compiled.load(machine.manifests, "tools");
|
|
compiled.load(machine.manifests, "dashboard");
|
|
compiled.load(machine.manifests, "menu");
|
|
compiled.compat = compatibility(machine);
|
|
return compiled;
|
|
}
|
|
|
|
cockpit.transport.wait(function() {
|
|
index.start();
|
|
});
|
|
}
|
|
|
|
function message_queue(event) {
|
|
window.messages.push(event);
|
|
}
|
|
|
|
/* When we're being loaded into the index window we have additional duties */
|
|
if (document.documentElement.getAttribute("class") === "index-page") {
|
|
/* Indicates to child frames that we are a cockpit1 router frame */
|
|
window.name = "cockpit1";
|
|
|
|
/* The same thing as above, but compatibility with old cockpit */
|
|
window.options = { sink: true, protocol: "cockpit1" };
|
|
|
|
/* Tell the pages about our features. */
|
|
window.features = {
|
|
navbar_is_for_current_machine: true
|
|
};
|
|
|
|
/* While the index is initializing, snag any messages we receive from frames */
|
|
window.messages = [];
|
|
|
|
window.messages.cancel = function() {
|
|
window.removeEventListener("message", message_queue, false);
|
|
window.messages = null;
|
|
};
|
|
|
|
let language = document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1");
|
|
if (!language)
|
|
language = "en-us";
|
|
document.documentElement.lang = language;
|
|
|
|
window.addEventListener("message", message_queue, false);
|
|
}
|
|
|
|
export function machines_index(options, machines_ins, loader) {
|
|
return new MachinesIndex(options, machines_ins, loader);
|
|
}
|