system: Redesign system page

All css files under systemd/ directory where changed to less for
compatibility with the new system-global.less file.

Fixes #13121
Closes #13186
This commit is contained in:
Katerina Koukiou 2019-11-23 17:53:55 +01:00 committed by Marius Vollmer
parent b3d6873db9
commit f8c0966a6d
47 changed files with 1845 additions and 1422 deletions

View File

@ -216,7 +216,7 @@ tr.security.listing-ct-item {
margin: 10ex auto 0;
}
/* stolen from pkg/systemd/host.css */
/* stolen from pkg/systemd/overview.less */
.content-header-extra {
background: #f5f5f5;
border-bottom: 1px solid #ddd;

View File

@ -571,6 +571,7 @@ function setup() {
var link = $("<a tabindex='0'>");
element.append(link);
var hostname_link = $("#system_information_hostname_button");
var hostname_tooltip = $("#system_information_hostname_tooltip");
var realmd = null;
var realms = null;
@ -600,10 +601,13 @@ function setup() {
if (!joined || !joined.length) {
text = _("Join Domain");
hostname_link.removeAttr('disabled');
hostname_tooltip.removeAttr('title');
hostname_tooltip.removeAttr('data-original-title');
} else {
text = joined.map(function(x) { return x.Name }).join(", ");
hostname_link.attr('disabled', 'disabled');
hostname_link.attr('title', _("Host name should not be changed in a domain")).tooltip('fixTitle');
hostname_tooltip.attr('title', _("Host name should not be changed in a domain"))
.tooltip('fixTitle');
}
link.text(text);
}

View File

@ -1,75 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 $ from "jquery";
import cockpit from "cockpit";
/*
* INITIALIZATION AND NAVIGATION
*
* The code above still uses the legacy 'Page' abstraction for both
* pages and dialogs, and expects page.setup, page.enter, page.show,
* and page.leave to be called at the right times.
*
* We cater to this with a little compatability shim consisting of
* 'dialog_setup', 'page_show', and 'page_hide'.
*/
export function page_show(p, arg) {
if (!p._entered_) {
p.enter(arg);
}
p._entered_ = true;
$('#' + p.id)
.show()
.removeAttr("hidden");
p.show();
}
export function set_page_link(element_sel, page, text) {
if (cockpit.manifests[page]) {
var link = document.createElement("a");
link.innerHTML = text;
link.tabIndex = 0;
link.addEventListener("click", function() { cockpit.jump("/" + page) });
$(element_sel).html(link);
} else {
$(element_sel).text(text);
}
}
export function dialog_setup(d) {
d.setup();
$('#' + d.id)
.on('show.bs.modal', function(event) {
if (event.target.id === d.id)
d.enter();
})
.on('shown.bs.modal', function(event) {
if (event.target.id === d.id)
d.show();
})
.on('hidden.bs.modal', function(event) {
if (event.target.id === d.id)
d.leave();
});
}
export function page_hide(p) {
$('#' + p.id).hide();
}

View File

@ -1,198 +0,0 @@
@import "../lib/table.css";
@import "../lib/cockpit-components-empty-state.css";
body {
/* Work around a pesky scrollbar on the page, due to 100% height */
height: auto;
}
.server-overview {
display: grid;
grid-gap: 0 2rem;
/* By default, horizontally stack the grid elements */
grid-template-areas: "motd" "info";
grid-template-columns: 1fr;
}
@media screen and (min-width: 640px) {
/* Lay out overview in a grid, if there's enough width */
.server-overview {
grid-template-areas: "motd motd";
grid-template-columns: auto 1fr;
}
}
.server-overview > .info-table-ct-container {
grid-area: info;
}
.server-overview > .motd-box {
grid-area: motd;
}
.systime-inline form .pficon-close,
.systime-inline form .fa-plus {
height: 26px;
width: 26px;
padding: 4px;
float: right;
margin-left: 5px;
}
.systime-inline .form-inline {
background: #f4f4f4;
border-width: 0 1px 1px 1px;
border-style: solid;
border-color: #bababa;
padding: 4px;
}
.systime-inline .form-inline:first-of-type {
border-top: 1px solid #bababa;
}
.systime-inline .form-control {
margin: 0 4px;
}
.systime-inline .form-group:first-of-type .form-control {
margin: 0 4px 0 0;
}
.systime-inline .form-group .form-control {
width: 214px;
}
/* Make sure error message don't overflow the dialog */
.realms-op-diagnostics {
max-width: 550px;
text-align: left;
max-height: 200px;
}
.realms-op-wait-message {
margin-left: 10px;
float: left;
margin-top: 3px;
}
.realms-op-error .realms-op-more-diagnostics {
font-weight: normal;
}
/* leave some space between form and leave toggle */
#realms-op-leave-toggle {
font-weight: bold;
line-height: 5rem;
}
/* standard PF alerts have a wide margin */
.realms-op-leave-only-row .pf-c-alert {
padding-left: 2ex;
}
.realms-op-leave-only-row .pf-c-alert button {
margin: 1ex 0;
}
/* Other styles */
.small-messages {
font-size: smaller;
}
#sich-note-1,
#sich-note-2 {
margin: 0;
}
#system_information_ssh_keys .list-group-item {
cursor: auto;
}
#system_information_hardware_text,
#system_information_os_text,
#system-information-enable-pcp-link {
overflow: visible;
white-space: normal;
word-wrap: break-word;
}
.system-information-updates > .fa {
line-height: inherit;
}
#system_machine_id {
display: inline-block;
font-family: monospace;
word-break: break-all;
}
@media (min-width: 500px) {
.cockpit-modal-md {
width: 400px;
}
}
@media screen and (max-width: 960px) {
.service-unit-data {
white-space: normal;
}
}
#accordion-markup {
margin-bottom: 0px;
}
.nav-tabs-pf > li:first-child > a {
padding-left: 20px;
}
.nav li a {
font-size: 13px;
}
.detail_table > tbody > tr > td, .detail_table > thead > tr > th {
padding-right: 10px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
#motd {
background-color: transparent;
border: none;
font-size: 14px;
padding: 0px;
margin: 0px;
white-space: pre-wrap;
}
.full-width {
width: 100%;
}
table.reporting-table tr:first-child td {
border-top: none;
}
table.reporting-table td:first-child {
white-space: nowrap;
width: 40%;
}
table.reporting-table td:nth-child(2) {
text-align: start;
width: 60%;
}
table.reporting-table td:nth-child(2) > .spinner {
display: inline-block;
margin-right: 0.5em;
vertical-align: middle;
}
td.report-column {
flex-direction: row-reverse;
}

View File

@ -294,7 +294,7 @@ class HardwareInfo extends React.Component {
<div className="page-ct container-fluid">
<CPUSecurityMitigationsDialog show={this.state.showCpuSecurityDialog} onClose={ () => this.setState({ showCpuSecurityDialog: false }) } />
<ol className="breadcrumb">
<li><button role="link" className="link-button" onClick={ () => cockpit.jump("/system", cockpit.transport.host) }>{ _("System") }</button></li>
<li><button role="link" className="link-button" onClick={ () => cockpit.jump("/system", cockpit.transport.host) }>{ _("Overview") }</button></li>
<li className="active">{ _("Hardware Information") }</li>
</ol>

View File

@ -1,4 +1,4 @@
@import "./system-global.css";
@import "./system-global.less";
@import "../lib/table.css";
/* show tbodys side by side on wide screens */

View File

@ -1,422 +1,297 @@
<!DOCTYPE html>
<html>
<head>
<title translate="yes">System</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="../base1/patternfly.css" rel="stylesheet">
<link href="../shell/index.css" rel="stylesheet">
<link href="system.css" rel="stylesheet">
<script src="../base1/jquery.js"></script>
<script src="../base1/cockpit.js"></script>
<script src="../manifests.js"></script>
<script src="../*/po.js"></script>
<meta charset="utf-8">
<title>Overview</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../base1/patternfly.css">
<link rel="stylesheet" href="overview.css">
<script type="text/javascript" src="../base1/cockpit.js"></script>
<script type="text/javascript" src="../base1/jquery.js"></script>
<!-- <script type="text/javascript" src="d3.js"></script> -->
<script type="text/javascript" src="overview.js"></script>
<script type="text/javascript" src="../*/po.js"></script>
<script src="../manifests.js"></script>
</head>
<body class="pf-m-redhat-font" hidden>
<body class="pf-m-redhat-font">
<div id="overview"></div>
<script id="ssh-host-keys-tmpl" type="x-template/mustache">
<div class="list-group dialog-list-ct">
{{#keys}}
<div class="list-group-item">
<p>{{ title }}</p>
{{#fps}}
<small>{{.}}</small>
{{/fps}}
</div>
{{/keys}}
{{^keys}}
<div class="list-group-item">
<p translate="yes">No host keys found.</p>
</div>
{{/keys}}
</div>
</script>
<script id="ntp-status-icon-tmpl" type="x-template/mustache">
{{#Synched}}
<span class="fa fa-lg fa-info-circle"></span>
{{/Synched}}
{{^Synched}}
{{#Server}}
<span class="spinner spinner-xs spinner-inline"></span>
{{/Server}}
{{^Server}}
<span class="fa fa-lg fa-exclamation-circle"></span>
{{/Server}}
{{/Synched}}
</script>
<script id="ntp-status-tmpl" type="x-template/mustache">
{{#Synched}}
{{#Server}}
<div translate="yes">Synchronized with {{Server}}</div>
{{/Server}}
{{^Server}}
<div translate="yes">Synchronized</div>
{{/Server}}
{{/Synched}}
{{^Synched}}
{{#Server}}
<div translate="yes">Trying to synchronize with {{Server}}</div>
{{/Server}}
{{^Server}}
<div translate="yes">Not synchronized</div>
{{#service}}
<a tabindex="0" data-goto-service="{{.}}" class="small-messages" translate>Log messages</a>
{{/service}}
{{/Server}}
{{/Synched}}
{{#SubStatus}}
<div class="small-messages">{{SubStatus}}</div>
{{/SubStatus}}
</script>
<div id="server" class="container-fluid page-ct server-overview">
<div id="motd-box" class="motd-box" hidden>
<div class="pf-c-alert pf-m-info pf-m-inline" aria-label="Info alert">
<div class="pf-c-alert__icon">
<i class="fa fa-info-circle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title">
<pre id="motd"></pre>
</h4>
<div class="pf-c-alert__action">
<button class="pf-c-button pf-m-plain" type="button">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="info-table-ct-container">
<form class="ct-form">
<label class="control-label" for="system_information_hardware_text" translate="yes">Hardware</label>
<span> <!-- wrap the <a> so that it doesn't stretch to the whole page width; otherwise tooltip looks bad -->
<a tabindex="0" id="system_information_hardware_text"></a>
</span>
<label class="control-label" for="system_information_asset_tag_text" translate="yes">Asset Tag</label>
<span id="system_information_asset_tag_text"></span>
<label class="control-label" for="system_machine_id" translate="yes">Machine ID</label>
<span id="system_machine_id"></span>
<label class="control-label" for="system_information_os_text" translate="yes">Operating System</label>
<span id="system_information_os_text"></span>
<div role="group" class="system-information-updates">
<div>
<span id="system_information_updates_icon"></span>
<span id="system_information_updates_text"></span>
</div>
<br>
<div>
<span id="insights_icon"></span>
<span id="insights_text"></span>
</div>
</div>
<label class="control-label" for="system-ssh-keys-link" translate="yes">Secure Shell Keys</label>
<a tabindex="0" id="system-ssh-keys-link" translate="yes" data-toggle="modal"
data-target="#system_information_ssh_keys">Show fingerprints</a>
<label class="control-label hidden" for="system-ostree-version-link" translate="yes">Version</label>
<a tabindex="0" id="system-ostree-version-link" class="hidden"></a>
<label class="control-label" for="system_information_hostname_button" translate="yes">Host Name</label>
<span id="hostname-tooltip">
<a tabindex="0" class="hostname-privileged" id="system_information_hostname_button"></a>
</span>
<label class="control-label" for="system-info-domain" translate="yes" hidden>Domain</label>
<span id="system-info-domain" hidden></span>
<label class="control-label" for="system_information_systime_button" translate="yes">System Time</label>
<div role="group">
<span id="systime-tooltip">
<a tabindex="0" class="systime-privileged" id="system_information_systime_button"></a>
</span>
<a tabindex="0" hidden id="system_information_systime_ntp_status"
tabindex="0" role="button" data-toggle="tooltip"
data-placement="bottom" data-html="true" >
</a>
</div>
<label class="control-label" for="shutdown-action" translate="yes">Power Options</label>
<div id="shutdown-group" class="btn-group">
<button class="btn btn-default shutdown-privileged" id="shutdown-action" data-action="restart"
data-container="body" translate="yes">Restart</button>
<button data-toggle="dropdown" class="btn
btn-default dropdown-toggle
shutdown-privileged"
aria-labelledby="system_information_power_options_label">
<i class="fa fa-caret-down pf-c-context-selector__toggle-icon" aria-hidden="true"></i>
</button>
<ul role="menu" class="dropdown-menu">
<li class="presentation">
<a tabindex="0" role="menuitem" data-action="restart" translate="yes">Restart</a>
</li>
<li class="presentation">
<a tabindex="0" role="menuitem" data-action="shutdown" translate="yes">Shut Down</a>
</li>
</ul>
</div>
<label class="control-label" for="tuned-status-button" translate="yes">Performance Profile</label>
<span id="system-info-performance" hidden></span>
<label class="control-label" for="server-pmlogger-switch" translate="yes" hidden>Store Metrics</label>
<div id="server-pmlogger-switch" hidden>
</div>
<a tabindex="0" id="system-information-enable-pcp-link" hidden>
<span class="pficon pficon-info"></span>
<span translate>Enable stored metrics…</span>
</a>
<label class="control-label" for="page_status_notifications" translate="yes">System Health</label>
<div id="page_status_notifications">
</div>
</form>
</div>
<div class="modal" id="system_information_ssh_keys" tabindex="-1"
role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" translate="yes">Machine SSH Key Fingerprints</h4>
</div>
<div class="modal-body">
<div class="spinner spinner-lg"></div>
<div class="pf-c-alert pf-m-danger pf-m-inline dialog-error" aria-label="inline danger alert" hidden>
<div class="pf-c-alert__icon">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title"></h4>
</div>
<div class="content" hidden></div>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" translate>Close</button>
</div>
</div>
<script id="ntp-status-icon-tmpl" type="x-template/mustache">
{{#Synched}}
<span class="fa fa-lg fa-info-circle"></span>
{{/Synched}}
{{^Synched}}
{{#Server}}
<span class="spinner spinner-xs spinner-inline"></span>
{{/Server}}
{{^Server}}
<span class="fa fa-lg fa-exclamation-circle"></span>
{{/Server}}
{{/Synched}}
</script>
<script id="ntp-status-tmpl" type="x-template/mustache">
{{#Synched}}
{{#Server}}
<div translate="yes">Synchronized with {{Server}}</div>
{{/Server}}
{{^Server}}
<div translate="yes">Synchronized</div>
{{/Server}}
{{/Synched}}
{{^Synched}}
{{#Server}}
<div translate="yes">Trying to synchronize with {{Server}}</div>
{{/Server}}
{{^Server}}
<div translate="yes">Not synchronized</div>
{{#service}}
<a tabindex="0" data-goto-service="{{.}}" class="small-messages" translate>Log messages</a>
{{/service}}
{{/Server}}
{{/Synched}}
{{#SubStatus}}
<div class="small-messages">{{SubStatus}}</div>
{{/SubStatus}}
</script>
<script id="ntp-servers-tmpl" type="x-template/mustache">
<div class="systime-inline">
{{#NTPServers}}
<form class="form-inline">
<button data-action="add" data-index="{{index}}" class="btn btn-default fa fa-plus"></button>
<button data-action="del" data-index="{{index}}" class="btn btn-default pficon-close"></button>
<div class="form-group">
<input type="text" class="form-control" value="{{Value}}" placeholder="{{Placeholder}}">
</div>
</form>
{{/NTPServers}}
</div>
</script>
<div class="modal" id="system_information_change_hostname" tabindex="-1"
role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" translate>Change Host Name</h4>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td>
<label class="control-label" for="sich-pretty-hostname"
translate="yes">Pretty Host Name</label>
</td>
<td>
<input id="sich-pretty-hostname" class="form-control">
</td>
</tr>
<tr>
<td>
<label class="control-label" for="sich-hostname"
translate="yes">Real Host Name</label>
</td>
<td>
<div id=sich-hostname-error>
<input id="sich-hostname" class="form-control">
</div>
</td>
</tr>
<tr>
<td></td>
<td>
<div class="has-error">
<span id="sich-note-1" class="help-block"></span>
</div>
</td>
</tr>
<tr>
<td></td>
<td>
<div class="has-error">
<span id="sich-note-2" class="help-block"></span>
</div>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" translate>Cancel</button>
<button class="btn btn-primary" id="sich-apply-button" translate>Change</button>
</div>
</div>
</div>
</div>
<script id="ntp-servers-tmpl" type="x-template/mustache">
<div class="systime-inline">
{{#NTPServers}}
<form class="form-inline">
<button data-action="add" data-index="{{index}}" class="btn btn-default fa fa-plus"></button>
<button data-action="del" data-index="{{index}}" class="btn btn-default pficon-close"></button>
<div class="form-group">
<input type="text" class="form-control" value="{{Value}}" placeholder="{{Placeholder}}">
<div class="modal" id="system_information_change_systime" tabindex="-1"
role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" translate>Change System Time</h4>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td>
<label class="control-label" for="systime-timezones" translate="yes">Time Zone</label>
</td>
<td>
<select class="form-control" id="systime-timezones">
</select>
</td>
</tr>
<tr>
<td></td>
<td class="has-error">
<span id="systime-timezone-error" class="help-block" translate="yes">Invalid time zone</span>
</td>
</tr>
<tr>
<td><label class="control-label" for="change_systime"
translate="yes">Set Time</label></td>
<td>
<div class="btn-group bootstrap-select dropdown form-control" id="change_systime">
<button class="btn btn-default dropdown-toggle" type="button"
data-toggle="dropdown">
<span class="pull-left" translate="yes">Manually</span>
<i class="fa fa-caret-down pf-c-context-selector__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu">
<li value="manual_time"><a tabindex="0" translate="yes">Manually</a></li>
<li value="ntp_time"><a tabindex="0" translate="yes">Automatically using NTP</a></li>
<li value="ntp_time_custom"><a tabindex="0" translate="yes">Automatically using specific NTP servers</a></li>
</ul>
</div>
</td>
</tr>
<tr id="systime-manual-row">
<td></td>
<td>
<input type='text' class="form-control" id="systime-date-input"/>
<input type='text' class="form-control" id="systime-time-hours"/>
:
<input type='text' class="form-control" id="systime-time-minutes"/>
</td>
</tr>
<tr id="systime-manual-error-row">
<td>
</td>
<td class="has-error">
<span id="systime-parse-error" class="help-block"></span>
</td>
</tr>
<tr id="systime-ntp-servers-row">
<td></td>
<td id="systime-ntp-servers">
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" translate>Cancel</button>
<button class="btn btn-primary" id="systime-apply-button" translate>Change</button>
</div>
</div>
</form>
{{/NTPServers}}
</div>
</script>
</div>
<div class="modal" id="system_information_change_systime" tabindex="-1"
role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" translate>Change System Time</h4>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td>
<label class="control-label" for="systime-timezones" translate="yes">Time Zone</label>
</td>
<td>
<select class="form-control" id="systime-timezones">
</select>
</td>
</tr>
<tr>
<td></td>
<td class="has-error">
<span id="systime-timezone-error" class="help-block" translate="yes">Invalid time zone</span>
</td>
</tr>
<tr>
<td><label class="control-label" for="change_systime"
translate="yes">Set Time</label></td>
<td>
<div class="btn-group bootstrap-select dropdown form-control" id="change_systime">
<button class="btn btn-default dropdown-toggle" type="button"
data-toggle="dropdown">
<span class="pull-left" translate="yes">Manually</span>
<i class="fa fa-caret-down pf-c-context-selector__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu">
<li value="manual_time"><a tabindex="0" translate="yes">Manually</a></li>
<li value="ntp_time"><a tabindex="0" translate="yes">Automatically using NTP</a></li>
<li value="ntp_time_custom"><a tabindex="0" translate="yes">Automatically using specific NTP servers</a></li>
</ul>
</div>
</td>
</tr>
<tr id="systime-manual-row">
<td></td>
<td>
<input type='text' class="form-control" id="systime-date-input"/>
<input type='text' class="form-control" id="systime-time-hours"/>
:
<input type='text' class="form-control" id="systime-time-minutes"/>
</td>
</tr>
<tr id="systime-manual-error-row">
<td>
</td>
<td class="has-error">
<span id="systime-parse-error" class="help-block"></span>
</td>
</tr>
<tr id="systime-ntp-servers-row">
<td></td>
<td id="systime-ntp-servers">
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" translate>Cancel</button>
<button class="btn btn-primary" id="systime-apply-button" translate>Change</button>
</div>
</div>
</div>
</div>
<script id="ssh-host-keys-tmpl" type="x-template/mustache">
<div class="list-group dialog-list-ct">
{{#keys}}
<div class="list-group-item">
<p>{{ title }}</p>
{{#fps}}
<small>{{.}}</small>
{{/fps}}
</div>
{{/keys}}
{{^keys}}
<div class="list-group-item">
<p translate="yes">No host keys found.</p>
</div>
{{/keys}}
</div>
</script>
<div class="modal" id="system_information_ssh_keys" tabindex="-1"
role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" translate="yes">Machine SSH Key Fingerprints</h4>
</div>
<div class="modal-body">
<div class="spinner spinner-lg"></div>
<div class="pf-c-alert pf-m-danger pf-m-inline dialog-error" aria-label="inline danger alert" hidden>
<div class="pf-c-alert__icon">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
</div>
<h4 class="pf-c-alert__title"></h4>
</div>
<div class="content" hidden></div>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" translate>Close</button>
</div>
</div>
</div>
</div>
<div class="modal" id="shutdown-dialog" tabindex="-1" role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title"></h4>
</div>
<div class="modal-body">
<textarea class="form-control">
</textarea>
<table>
<tr>
<td>
<label translate="yes">Delay</label>
</td>
<td>
<div class="btn-group bootstrap-select dropdown form-control">
<button class="btn btn-default dropdown-toggle" type="button"
data-toggle="dropdown">
<span class="pull-left" translate="yes">1 Minute</span>
<i class="fa fa-caret-down pf-c-context-selector__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu">
<li value="1"><a tabindex="0" translate="yes">1 Minute</a></li>
<li value="5"><a tabindex="0" translate="yes">5 Minutes</a></li>
<li value="20"><a tabindex="0" translate="yes">20 Minutes</a></li>
<li value="40"><a tabindex="0" translate="yes">40 Minutes</a></li>
<li value="60"><a tabindex="0" translate="yes">60 Minutes</a></li>
<li role="separator" class="divider"></li>
<li value="0"><a tabindex="0" translate="yes">No Delay</a></li>
<li value="x"><a tabindex="0" translate="yes">Specific Time</a></li>
</ul>
</div>
</td>
<td>
<div>
<input class="form-control shutdown-date" type="text">
<input class="form-control shutdown-hours" type="text">
:
<input class="form-control shutdown-minutes" type="text">
</div>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default" translate="yes" data-dismiss="modal">Cancel</button>
<button class="btn btn-danger"></button>
</div>
</div>
</div>
</div>
<div class="modal" id="system_information_change_hostname" tabindex="-1"
role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" translate>Change Host Name</h4>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td>
<label class="control-label" for="sich-pretty-hostname"
translate="yes">Pretty Host Name</label>
</td>
<td>
<input id="sich-pretty-hostname" class="form-control">
</td>
</tr>
<tr>
<td>
<label class="control-label" for="sich-hostname"
translate="yes">Real Host Name</label>
</td>
<td>
<div id=sich-hostname-error>
<input id="sich-hostname" class="form-control">
</div>
</td>
</tr>
<tr>
<td></td>
<td>
<div class="has-error">
<span id="sich-note-1" class="help-block"></span>
</div>
</td>
</tr>
<tr>
<td></td>
<td>
<div class="has-error">
<span id="sich-note-2" class="help-block"></span>
</div>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default" data-dismiss="modal" translate>Cancel</button>
<button class="btn btn-primary" id="sich-apply-button" translate>Change</button>
</div>
</div>
</div>
</div>
<div class="modal" id="shutdown-dialog" tabindex="-1" role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title"></h4>
</div>
<div class="modal-body">
<textarea class="form-control">
</textarea>
<table>
<tr>
<td>
<label translate="yes">Delay</label>
</td>
<td>
<div class="btn-group bootstrap-select dropdown form-control">
<button class="btn btn-default dropdown-toggle" type="button"
data-toggle="dropdown">
<span class="pull-left" translate="yes">1 Minute</span>
<i class="fa fa-caret-down pf-c-context-selector__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu">
<li value="1"><a tabindex="0" translate="yes">1 Minute</a></li>
<li value="5"><a tabindex="0" translate="yes">5 Minutes</a></li>
<li value="20"><a tabindex="0" translate="yes">20 Minutes</a></li>
<li value="40"><a tabindex="0" translate="yes">40 Minutes</a></li>
<li value="60"><a tabindex="0" translate="yes">60 Minutes</a></li>
<li role="separator" class="divider"></li>
<li value="0"><a tabindex="0" translate="yes">No Delay</a></li>
<li value="x"><a tabindex="0" translate="yes">Specific Time</a></li>
</ul>
</div>
</td>
<td>
<div>
<input class="form-control shutdown-date" type="text">
<input class="form-control shutdown-hours" type="text">
:
<input class="form-control shutdown-minutes" type="text">
</div>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default" translate="yes" data-dismiss="modal">Cancel</button>
<button class="btn btn-danger"></button>
</div>
</div>
</div>
</div>
<div class="modal" id="confirmation-dialog" tabindex="-1" role="dialog" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="confirmation-dialog-title"></h4>
</div>
<div class="modal-body" id="confirmation-dialog-body">
</div>
<div class="modal-footer">
<button class="btn btn-default" translate="yes" id="confirmation-dialog-cancel">Cancel</button>
<button class="btn btn-danger" id="confirmation-dialog-confirm">
</button>
</div>
</div>
</div>
</div>
<script src="system.js"></script>
<script src="../domain/domain.js"></script>
<script src="../performance/performance.js"></script>
<script src="../domain/domain.js"></script>
<script src="../performance/performance.js"></script>
</body>
</html>

View File

@ -27,8 +27,6 @@ import ReactDOM from 'react-dom';
import React from 'react';
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import "./logs.css";
$(function() {
cockpit.translate();
const _ = cockpit.gettext;

View File

@ -1,6 +1,6 @@
@import "../lib/table.css";
@import "../lib/journal.css";
@import "./system-global.css";
@import "./system-global.less";
/* Make sure to not break log message lines in order to preserve information */
#journal-entry .info-table-ct td {

View File

@ -8,7 +8,7 @@
"menu": {
"index": {
"label": "System",
"label": "Overview",
"order": 10,
"keywords": [
{

View File

@ -1,7 +1,7 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2013 Red Hat, Inc.
* Copyright (C) 2019 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
@ -16,273 +16,116 @@
* 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 { mustache } from "mustache";
import $ from "jquery";
import React from 'react';
import { Card, CardHeader, CardBody, Button } from '@patternfly/react-core';
import React from "react";
import ReactDOM from "react-dom";
import { OnOffSwitch } from "cockpit-components-onoff.jsx";
import "polyfills.js";
import * as service from "service.js";
import host_keys_script from "raw-loader!./ssh-list-host-keys.sh";
import cockpit from "cockpit";
import * as machine_info from "machine-info.js";
import $ from "jquery";
import { mustache } from "mustache";
import * as packagekit from "packagekit.js";
import { install_dialog } from "cockpit-components-install-dialog.jsx";
import * as service from "service.js";
import { shutdown } from "./shutdown.js";
import host_keys_script from "raw-loader!./ssh-list-host-keys.sh";
import { page_status } from "notifications";
import { set_page_link, dialog_setup, page_show } from './helpers.js';
import { PageStatusNotifications } from "./page-status.jsx";
import "form-layout.less";
import { ServerTime } from './serverTime.js';
/* These add themselves to jQuery so just including is enough */
import "patterns";
import "bootstrap-datepicker/dist/js/bootstrap-datepicker";
import "patternfly-bootstrap-combobox/js/bootstrap-combobox";
import "./configurationCard.less";
const _ = cockpit.gettext;
var permission = cockpit.permission({ admin: true });
$(permission).on("changed", update_hostname_privileged);
$(permission).on("changed", update_shutdown_privileged);
$(permission).on("changed", update_systime_privileged);
function update_hostname_privileged() {
$(".hostname-privileged").update_privileged(
permission, cockpit.format(
_("The user <b>$0</b> is not permitted to modify hostnames"),
permission.user ? permission.user.name : ''), null, $('#hostname-tooltip')
);
function dialog_setup(d) {
d.setup();
$('#' + d.id)
.on('show.bs.modal', function(event) {
if (event.target.id === d.id)
d.enter();
})
.on('shown.bs.modal', function(event) {
if (event.target.id === d.id)
d.show();
})
.on('hidden.bs.modal', function(event) {
if (event.target.id === d.id)
d.leave();
});
}
function update_shutdown_privileged() {
$(".shutdown-privileged").update_privileged(
permission, cockpit.format(
_("The user <b>$0</b> is not permitted to shutdown or restart this server"),
permission.user ? permission.user.name : '')
);
}
export class ConfigurationCard extends React.Component {
constructor(props) {
super(props);
this.state = {
pmlogger_switch_visible: false,
pcp_link_visible: false,
serverTime: '',
};
function update_systime_privileged() {
$(".systime-privileged").update_privileged(
permission, cockpit.format(
_("The user <b>$0</b> is not permitted to change the system time"),
permission.user ? permission.user.name : ''), null, $('#systime-tooltip')
);
}
this.pmcd_service = service.proxy("pmcd");
this.pmlogger_service = service.proxy("pmlogger");
this.pmlogger_exists = false;
this.packagekit_exists = false;
function debug() {
if (window.debugging == "all" || window.debugging == "system")
console.debug.apply(console, arguments);
}
this.onPmLoggerSwitchChange = this.onPmLoggerSwitchChange.bind(this);
this.update_pmlogger_row = this.update_pmlogger_row.bind(this);
this.pmlogger_service_changed = this.pmlogger_service_changed.bind(this);
function ServerTime() {
var self = this;
this.host_keys_show = this.host_keys_show.bind(this);
this.host_keys_hide = this.host_keys_hide.bind(this);
}
var client = cockpit.dbus('org.freedesktop.timedate1');
var timedate = client.proxy();
componentDidMount() {
dialog_setup(new PageSystemInformationChangeHostname());
permission.addEventListener("changed", this.update_hostname_privileged);
this.update_hostname_privileged();
var time_offset = null;
var remote_offset = null;
dialog_setup(this.change_systime_dialog = new PageSystemInformationChangeSystime());
this.systime_setup();
permission.addEventListener("changed", this.update_systime_privileged);
this.update_systime_privileged();
this.client = client;
self.timedate = timedate;
this.ntp_waiting_value = null;
this.ntp_waiting_resolve = null;
self.timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
self.timesyncd_service = service.proxy("systemd-timesyncd.service");
/*
* The time we return from here as its UTC time set to the
* server time. This is the only way to get predictable
* behavior and formatting of a Date() object in the absence of
* IntlDateFormat and friends.
*/
Object.defineProperty(self, 'utc_fake_now', {
enumerable: true,
get: function get() {
var offset = time_offset + remote_offset;
return new Date(offset + (new Date()).valueOf());
}
});
Object.defineProperty(self, 'now', {
enumerable: true,
get: function get() {
return new Date(time_offset + (new Date()).valueOf());
}
});
self.format = function format(and_time) {
var string = self.utc_fake_now.toISOString();
if (!and_time)
return string.split('T')[0];
var pos = string.lastIndexOf(':');
if (pos !== -1)
string = string.substring(0, pos);
return string.replace('T', ' ');
};
self.updateInterval = window.setInterval(function() {
$(self).triggerHandler("changed");
}, 30000);
self.wait = function wait() {
if (remote_offset === null)
return self.update();
return cockpit.resolve();
};
self.update = function update() {
return cockpit.spawn(["date", "+%s:%z"], { err: "message" })
.done(function(data) {
const parts = data.trim().split(":");
const timems = parseInt(parts[0], 10) * 1000;
let tzmin = parseInt(parts[1].slice(-2), 10);
const tzhour = parseInt(parts[1].slice(0, -2));
if (tzhour < 0)
tzmin = -tzmin;
const offsetms = (tzhour * 3600000) + tzmin * 60000;
const now = new Date();
time_offset = (timems - now.valueOf());
remote_offset = offsetms;
$(self).triggerHandler("changed");
})
.fail(function(ex) {
console.log("Couldn't calculate server time offset: " + cockpit.message(ex));
});
};
self.change_time = function change_time(datestr, hourstr, minstr) {
var dfd = $.Deferred();
/*
* The browser is brain dead when it comes to dates. But even if
* it wasn't, or we loaded a library like moment.js, there is no
* way to make sense of this date without a round trip to the
* server ... the timezone is really server specific.
*/
cockpit.spawn(["date", "--date=" + datestr + " " + hourstr + ":" + minstr, "+%s"])
.fail(function(ex) {
dfd.reject(ex);
})
.done(function(data) {
var seconds = parseInt(data.trim(), 10);
timedate.call('SetTime', [seconds * 1000 * 1000, false, true])
.fail(function(ex) {
dfd.reject(ex);
})
.done(function() {
self.update();
dfd.resolve();
});
});
return dfd;
};
self.poll_ntp_synchronized = function poll_ntp_synchronized() {
client.call(timedate.path,
"org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTPSynchronized"])
.fail(function(error) {
if (error.name != "org.freedesktop.DBus.Error.UnknownProperty" &&
error.problem != "not-found")
console.log("can't get NTPSynchronized property", error);
})
.done(function(result) {
var ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
var data = { };
data[timedate.path] = ifaces;
client.notify(data);
});
};
self.ntp_updated = function ntp_updated(path, iface, member, args) {
if (!self.ntp_waiting_resolve || !args[1].NTP)
return;
if (self.ntp_waiting_value !== args[1].NTP.v)
console.warn("Unexpected value of NTP");
self.ntp_waiting_resolve();
self.ntp_waiting_resolve = null;
};
self.close = function close() {
client.close();
};
self.update();
}
var change_systime_dialog;
PageServer.prototype = {
_init: function() {
this.id = "server";
this.server_time = null;
this.client = null;
this.hostname_proxy = null;
this.unregistered = false;
},
getTitle: function() {
return null;
},
setup: function() {
var self = this;
update_hostname_privileged();
cockpit.file("/etc/motd").watch(function(content) {
if (content)
content = content.trimRight();
if (content && content != cockpit.localStorage.getItem('dismissed-motd')) {
$('#motd').text(content);
$('#motd-box').show();
} else {
$('#motd-box').hide();
}
// To help the tests known when we have loaded motd
$('#motd-box').attr('data-stable', 'yes');
$(this.pmlogger_service).on("changed", data => this.pmlogger_service_changed());
this.pmlogger_service_changed();
packagekit.detect().then(exists => {
this.packagekit_exists = exists;
this.update_pmlogger_row();
});
$('#motd-box button.pf-c-button').click(function() {
cockpit.localStorage.setItem('dismissed-motd', $('#motd').text());
$('#motd-box').hide();
});
$("#system_information_ssh_keys").on("hide.bs.modal", () => this.host_keys_hide());
}
$('#shutdown-group [data-action]').on("click", function(ev) {
// don't let the click "fall through" to the dialog that we are about to open
ev.preventDefault();
self.shutdown($(this).attr('data-action'));
});
update_hostname_privileged() {
$(".hostname-privileged").update_privileged(
permission, cockpit.format(
_("The user <b>$0</b> is not permitted to modify hostnames"),
permission.user ? permission.user.name : ''),
null, $("#system_information_hostname_tooltip")
);
// this really needs the disabled attribute, not the disabled class
if (permission.allowed === false)
$(".hostname-privileged").attr("disabled", "disabled");
else
$(".hostname-privileged").removeAttr("disabled");
}
$('#system-ostree-version-link').on('click', function() {
cockpit.jump("/updates", cockpit.transport.host);
});
update_systime_privileged() {
$(".systime-privileged").update_privileged(
permission, cockpit.format(
_("The user <b>$0</b> is not permitted to change the system time"),
permission.user ? permission.user.name : ''), null, $('#systime-tooltip')
);
}
$('#system_information_hostname_button').on('click', function(ev) {
// you can't disable standard links, so implement this manually; realmd might disable host name changing
if (ev.target.getAttribute("disabled")) {
ev.preventDefault();
return;
}
PageSystemInformationChangeHostname.client = self.client;
$('#system_information_change_hostname').modal('show');
});
$('#system_information_systime_button').on('click', function() {
change_systime_dialog.display(self.server_time);
});
systime_setup() {
const self = this;
self.server_time = new ServerTime();
$(self.server_time).on("changed", function() {
$('#system_information_systime_button').text(self.server_time.format(true));
self.setState({ serverTime: self.server_time.format(true) });
});
self.server_time.client.subscribe({
@ -296,17 +139,6 @@ PageServer.prototype = {
self.ntp_status_icon_tmpl = $("#ntp-status-icon-tmpl").html();
mustache.parse(this.ntp_status_icon_tmpl);
self.ssh_host_keys_tmpl = $("#ssh-host-keys-tmpl").html();
mustache.parse(this.ssh_host_keys_tmpl);
$("#system_information_ssh_keys").on("show.bs.modal", function() {
self.host_keys_show();
});
$("#system_information_ssh_keys").on("hide.bs.modal", function() {
self.host_keys_hide();
});
var $ntp_status = $('#system_information_systime_ntp_status');
function update_ntp_status() {
@ -349,7 +181,7 @@ PageServer.prototype = {
$ntp_status.attr("data-original-title", tooltip_html);
var icon_html = mustache.render(self.ntp_status_icon_tmpl, model);
$ntp_status.html(icon_html);
self.setState({ ntp_status_icon: { __html: icon_html } });
}
$ntp_status.tooltip();
@ -364,258 +196,11 @@ PageServer.prototype = {
window.setInterval(function() {
self.server_time.poll_ntp_synchronized();
}, 5000);
}
$('#server').on('click', "[data-goto-service]", function() {
var service = $(this).attr("data-goto-service");
cockpit.jump("/system/services/#/" + window.encodeURIComponent(service),
cockpit.transport.host);
});
var pmcd_service = service.proxy("pmcd");
var pmlogger_service = service.proxy("pmlogger");
var pmlogger_promise;
var pmlogger_exists = false;
var packagekit_exists = false;
function update_pmlogger_row(force_disable) {
var logger_switch = $("#server-pmlogger-switch");
var enable_pcp = $('#system-information-enable-pcp-link');
if (!pmlogger_exists) {
enable_pcp.toggle(packagekit_exists);
logger_switch.hide();
logger_switch.prev().hide();
} else if (!pmlogger_promise) {
enable_pcp.hide();
logger_switch.show();
logger_switch.prev().show();
}
ReactDOM.render(
React.createElement(OnOffSwitch, {
state: pmlogger_service.state === "running",
disabled: pmlogger_service.state == "starting" || force_disable,
onChange: onPmLoggerSwitchChange
}),
document.getElementById('server-pmlogger-switch')
);
}
function pmlogger_service_changed() {
pmlogger_exists = pmlogger_service.exists;
/* HACK: The pcp packages on Ubuntu and Debian include SysV init
* scripts in /etc, which stay around when removing (as opposed to
* purging) the package. Systemd treats those as valid units, even
* if they're not backed by packages anymore. Thus,
* pmlogger_service.exists will be true. Check for the binary
* directly to make sure the package is actually available.
*/
if (pmlogger_exists) {
cockpit.spawn(["which", "pmlogger"], { err: "ignore" })
.fail(function() {
pmlogger_exists = false;
})
.always(() => update_pmlogger_row());
} else {
update_pmlogger_row();
}
}
packagekit.detect().then(function(exists) {
packagekit_exists = exists;
update_pmlogger_row();
});
function onPmLoggerSwitchChange(enable) {
if (!pmlogger_exists)
return;
update_pmlogger_row(true);
if (enable) {
pmlogger_promise = Promise.all([
pmcd_service.enable(),
pmcd_service.start(),
pmlogger_service.enable(),
pmlogger_service.start()
])
.catch(function(error) {
console.warn("Enabling pmlogger failed", error);
});
} else {
pmlogger_promise = Promise.all([pmlogger_service.disable(), pmlogger_service.stop()])
.catch(function(error) {
console.warn("Disabling pmlogger failed", error);
});
}
pmlogger_promise.finally(function() {
pmlogger_promise = null;
pmlogger_service_changed();
});
}
$(pmlogger_service).on('changed', pmlogger_service_changed);
pmlogger_service_changed();
function refresh_os_updates_state() {
const status = page_status.get("updates") || { };
const details = status.details || { };
$("#system_information_updates_icon").attr("class", details.icon || "");
$("#system_information_updates_icon").toggle(!!details.icon);
set_page_link("#system_information_updates_text", details.link || "updates",
details.text || status.title || "");
}
refresh_os_updates_state();
$(page_status).on("changed", refresh_os_updates_state);
var insights_client_timer = service.proxy("insights-client.timer");
function refresh_insights_status() {
const subfeats = (cockpit.manifests.subscriptions && cockpit.manifests.subscriptions.features) || { };
if (subfeats.insights && insights_client_timer.exists && !insights_client_timer.enabled) {
$("#insights_icon").attr("class", "pficon pficon-warning-triangle-o");
set_page_link("#insights_text", "subscriptions", _("Not connected to Insights"));
$("#insights_icon, #insights_text").show();
} else {
$("#insights_icon, #insights_text").hide();
}
}
$(insights_client_timer).on("changed", refresh_insights_status);
refresh_insights_status();
// Only link from graphs to available pages
set_page_link("#link-disk", "storage", _("Disk I/O"));
set_page_link("#link-network", "network", _("Network Traffic"));
function toggle_health_label(visible) {
$('label[for="page_status_notifications"]').toggle(visible);
}
ReactDOM.render(React.createElement(PageStatusNotifications, { toggle_label: toggle_health_label }),
document.getElementById('page_status_notifications'));
},
enter: function() {
host_keys_show() {
var self = this;
var machine_id = cockpit.file("/etc/machine-id");
machine_id.read()
.done(function(content) {
$("#system_machine_id").text(content);
})
.fail(function(ex) {
console.error("Error reading machine id", ex);
})
.always(function() {
machine_id.close();
});
self.ostree_client = cockpit.dbus('org.projectatomic.rpmostree1',
{ superuser : true });
$(self.ostree_client).on("close", function() {
self.ostree_client = null;
});
self.sysroot = self.ostree_client.proxy('org.projectatomic.rpmostree1.Sysroot',
'/org/projectatomic/rpmostree1/Sysroot');
$(self.sysroot).on("changed", $.proxy(this, "sysroot_changed"));
self.client = cockpit.dbus('org.freedesktop.hostname1',
{ superuser : "try" });
self.hostname_proxy = self.client.proxy('org.freedesktop.hostname1',
'/org/freedesktop/hostname1');
self.kernel_hostname = null;
const asset_tag_text = $("#system_information_asset_tag_text");
const hardware_text = $("#system_information_hardware_text");
hardware_text.tooltip({ title: _("Click to see system hardware information"), placement: "bottom" });
machine_info.dmi_info()
.done(function(fields) {
let vendor = fields.sys_vendor;
let name = fields.product_name;
if (!vendor || !name) {
vendor = fields.board_vendor;
name = fields.board_name;
}
if (!vendor || !name)
hardware_text.text(_("Details"));
else
hardware_text.text(vendor + " " + name);
var present = !!(fields.product_serial || fields.chassis_serial);
asset_tag_text.text(fields.product_serial || fields.chassis_serial);
asset_tag_text.toggle(present);
asset_tag_text.prev().toggle(present);
})
.fail(function(ex) {
debug("couldn't read dmi info: " + ex);
hardware_text.text(_("Details"));
asset_tag_text.toggle(false);
asset_tag_text.prev().toggle(false);
});
function hostname_text() {
if (!self.hostname_proxy)
return;
var pretty_hostname = self.hostname_proxy.PrettyHostname;
var static_hostname = self.hostname_proxy.StaticHostname;
var str = self.kernel_hostname;
if (pretty_hostname && static_hostname && static_hostname != pretty_hostname)
str = pretty_hostname + " (" + static_hostname + ")";
else if (static_hostname)
str = static_hostname;
if (!str)
str = _("Set Host name");
var hostname_button = $("#system_information_hostname_button");
hostname_button.text(str);
if (!hostname_button.attr("disabled")) {
hostname_button
.attr("title", str)
.tooltip('fixTitle');
}
$("#system_information_os_text").text(self.hostname_proxy.OperatingSystemPrettyName || "");
}
cockpit.spawn(["hostname"], { err: "ignore" })
.done(function(output) {
self.kernel_hostname = $.trim(output);
hostname_text();
})
.fail(function(ex) {
hostname_text();
debug("couldn't read kernel hostname: " + ex);
});
$(self.hostname_proxy).on("changed", hostname_text);
},
show: function() {
},
leave: function() {
var self = this;
$(self.hostname_proxy).off();
self.hostname_proxy = null;
self.client.close();
self.client = null;
$(cockpit).off('.server');
$(self.sysroot).off();
self.sysroot = null;
if (self.ostree_client) {
self.ostree_client.close();
self.ostree_client = null;
}
},
host_keys_show: function() {
var self = this;
$("#system_information_ssh_keys .spinner").toggle(true);
$("#system_information_ssh_keys .content").toggle(false);
$("#system_information_ssh_keys .pf-c-alert").toggle(false);
@ -629,9 +214,14 @@ PageServer.prototype = {
self.host_keys_update();
}, 10 * 1000);
self.host_keys_update();
},
}
host_keys_update: function() {
host_keys_hide() {
window.clearInterval(this.host_keys_interval);
this.host_keys_interval = null;
}
host_keys_update() {
var self = this;
var parenthesis = /^\((.*)\)$/;
var spinner = $("#system_information_ssh_keys .spinner");
@ -677,6 +267,9 @@ PageServer.prototype = {
return { title: k, fps: keys[k] };
});
self.ssh_host_keys_tmpl = $("#ssh-host-keys-tmpl").html();
mustache.parse(self.ssh_host_keys_tmpl);
tmp = mustache.render(self.ssh_host_keys_tmpl, { keys: arr });
content.html(tmp);
spinner.toggle(false);
@ -690,51 +283,155 @@ PageServer.prototype = {
$("#system_information_ssh_keys .pf-c-alert h4").text(msg);
error.toggle(true);
});
},
}
host_keys_hide: function() {
var self = this;
window.clearInterval(self.host_keys_interval);
self.host_keys_interval = null;
},
onPmLoggerSwitchChange(enable) {
if (!this.pmlogger_exists)
return;
sysroot_changed: function() {
var self = this;
var link = $("#system-ostree-version-link");
this.update_pmlogger_row(true);
if (self.sysroot.Booted && self.ostree_client) {
var version = "";
self.ostree_client.call(self.sysroot.Booted,
"org.freedesktop.DBus.Properties", "Get",
['org.projectatomic.rpmostree1.OS', "BootedDeployment"])
.done(function(result) {
if (result && result[0]) {
var deployment = result[0].v;
if (deployment && deployment.version)
version = deployment.version.v;
}
})
.fail(function(ex) {
console.log(ex);
})
.always(function() {
link.toggleClass("hidden", !version);
link.prev().toggleClass("hidden", !version);
link.text(version);
});
if (enable) {
this.pmlogger_promise = Promise.all([
this.pmcd_service.enable(),
this.pmcd_service.start(),
this.pmlogger_service.enable(),
this.pmlogger_service.start()
]).catch(function(error) {
console.warn("Enabling pmlogger failed", error);
});
} else {
link.toggleClass("hidden", true);
link.text("");
this.pmlogger_promise = Promise.all([this.pmlogger_service.disable(), this.pmlogger_service.stop()])
.catch(function(error) {
console.warn("Disabling pmlogger failed", error);
});
}
},
this.pmlogger_promise.finally(() => {
this.pmlogger_promise = null;
this.pmlogger_service_changed();
});
}
shutdown: function(action_type) {
shutdown(action_type, this.server_time);
},
};
update_pmlogger_row(force_disable) {
if (!this.pmlogger_exists) {
this.setState({ pcp_link_visible: this.packagekit_exists });
this.setState({ pmlogger_switch_visible: false });
} else if (!this.pmlogger_promise) {
this.setState({ pcp_link_visible: false });
this.setState({ pmlogger_switch_visible: true });
}
this.setState({ pm_logger_switch_disabled: force_disable });
}
function PageServer() {
this._init();
pmlogger_service_changed() {
this.pmlogger_exists = this.pmlogger_service.exists;
/* HACK: The pcp packages on Ubuntu and Debian include SysV init
* scripts in /etc, which stay around when removing (as opposed to
* purging) the package. Systemd treats those as valid units, even
* if they're not backed by packages anymore. Thus,
* pmlogger_service.exists will be true. Check for the binary
* directly to make sure the package is actually available.
*/
if (this.pmlogger_exists) {
cockpit.spawn(["which", "pmlogger"], { err: "ignore" })
.fail(function() {
this.pmlogger_exists = false;
})
.always(() => this.update_pmlogger_row());
} else {
this.update_pmlogger_row();
}
}
render() {
return (
<Card className="system-configuration">
<CardHeader>{_("Configuration")}</CardHeader>
<CardBody>
<table className="pf-c-table pf-m-grid-md pf-m-compact">
<tbody>
<tr>
<th scope="row">{_("Hostname")}</th>
<td>
{this.props.hostname && <span id="system_information_hostname_text">{this.props.hostname}</span>}
<span id="system_information_hostname_tooltip">
<Button variant='link'
id="system_information_hostname_button"
className="hostname-privileged"
isInline
onClick={() => $('#system_information_change_hostname').modal('show')}
isDisabled={$('system_information_change_hostname').attr("disabled")}
aria-label="edit hostname">
{this.props.hostname !== "" ? _("edit") : _("Set Hostname")}
</Button>
</span>
</td>
</tr>
<tr>
<th scope="row">{_("System time")}</th>
<td>
<span id="systime-tooltip">
<Button variant="link" isInline className="systime-privileged"
id="system_information_systime_button"
onClick={() => this.change_systime_dialog.display(this.server_time)}>
{this.state.serverTime}
</Button>
</span>
<a tabIndex="0" hidden id="system_information_systime_ntp_status"
role="button" data-toggle="tooltip"
data-placement="bottom" data-html="true" dangerouslySetInnerHTML={this.state.ntp_status_icon} />
</td>
</tr>
<tr>
<th scope="row">{_("Domain")}</th>
<td><p id="system-info-domain" /></td>
</tr>
<tr>
<th scope="row">{_("Performance profile")}</th>
<td><span id="system-info-performance" /></td>
</tr>
<tr>
<th scope="row">{_("Secure Shell keys")}</th>
<td>
<Button variant="link" isInline id="system-ssh-keys-link" data-toggle="modal" onClick={this.host_keys_show}
data-target="#system_information_ssh_keys">{_("Show fingerprints")}</Button>
</td>
</tr>
{this.state.pmlogger_switch_visible &&
<tr>
<th scope="row">{_("Store metrics")}</th>
<td>
<OnOffSwitch
id="server-pmlogger-switch"
state={this.pmlogger_service.state === "running"}
disabled={this.pmlogger_service.state == "starting" || this.state.pm_logger_switch_disabled}
onChange={this.onPmLoggerSwitchChange} />
</td>
</tr>}
{this.state.pcp_link_visible &&
<tr>
<th scope="row">{_("PCP")}</th>
<td>
<a id="system-configuration-enable-pcp-link" onClick={() => install_dialog("cockpit-pcp")}>
<span className="pficon pficon-info" />
<span>{_("Enable stored metrics…")}</span>
</a>
</td>
</tr>}
</tbody>
</table>
</CardBody>
</Card>
);
}
}
PageSystemInformationChangeHostname.prototype = {
@ -749,17 +446,19 @@ PageSystemInformationChangeHostname.prototype = {
},
enter: function() {
var self = this;
self.hostname_proxy = PageSystemInformationChangeHostname.client.proxy();
self._initial_hostname = self.hostname_proxy.StaticHostname || "";
self._initial_pretty_hostname = self.hostname_proxy.PrettyHostname || "";
$("#sich-pretty-hostname").val(self._initial_pretty_hostname);
$("#sich-hostname").val(self._initial_hostname);
this._always_update_from_pretty = false;
this._update();
this.client = cockpit.dbus('org.freedesktop.hostname1',
{ superuser : "try" });
this.hostname_proxy = this.client.proxy();
this.hostname_proxy.wait()
.then(() => {
this._initial_hostname = this.hostname_proxy.StaticHostname || "";
this._initial_pretty_hostname = this.hostname_proxy.PrettyHostname || "";
$("#sich-pretty-hostname").val(this._initial_pretty_hostname);
$("#sich-hostname").val(this._initial_hostname);
this._update();
});
},
show: function() {
@ -771,13 +470,11 @@ PageSystemInformationChangeHostname.prototype = {
},
_on_apply_button: function(event) {
var self = this;
var new_full_name = $("#sich-pretty-hostname").val();
var new_name = $("#sich-hostname").val();
var one = self.hostname_proxy.call("SetStaticHostname", [new_name, true]);
var two = self.hostname_proxy.call("SetPrettyHostname", [new_full_name, true]);
var one = this.hostname_proxy.call("SetStaticHostname", [new_name, true]);
var two = this.hostname_proxy.call("SetPrettyHostname", [new_full_name, true]);
// We can't use Promise.all() here, because dialg expects a promise
// with a progress() method (see pkg/lib/patterns.js)
@ -1340,30 +1037,3 @@ PageSystemInformationChangeSystime.prototype = {
function PageSystemInformationChangeSystime() {
this._init();
}
$("#system_information_hardware_text").on("click", function() {
$("#system_information_hardware_text").tooltip("hide");
cockpit.jump("/system/hwinfo", cockpit.transport.host);
return false;
});
$("#system-information-enable-pcp-link").on("click", function() {
install_dialog("cockpit-pcp");
});
function init() {
var server_page;
cockpit.translate();
server_page = new PageServer();
server_page.setup();
dialog_setup(new PageSystemInformationChangeHostname());
dialog_setup(change_systime_dialog = new PageSystemInformationChangeSystime());
page_show(server_page);
$("body").removeAttr("hidden");
}
$(init);

View File

@ -0,0 +1,8 @@
#system_information_ssh_keys .list-group-item {
cursor: auto;
}
#system_information_hostname_text + span {
font: inherit;
margin-left: 0.5em;
}

View File

@ -0,0 +1,96 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 React from 'react';
import { Card, CardHeader, CardBody, CardFooter } from '@patternfly/react-core';
import cockpit from "cockpit";
import { page_status } from "notifications";
import { PageStatusNotifications } from "../page-status.jsx";
import * as service from "service.js";
import "./healthCard.less";
const _ = cockpit.gettext;
export class HealthCard extends React.Component {
constructor() {
super();
this.state = { insightsLinkVisible: false };
this.refresh_os_updates_state = this.refresh_os_updates_state.bind(this);
this.refresh_insights_status = this.refresh_insights_status.bind(this);
}
componentDidMount() {
page_status.addEventListener("changed", this.refresh_os_updates_state);
this.refresh_os_updates_state();
this.insights_client_timer = service.proxy("insights-client.timer");
this.insights_client_timer.addEventListener("changed", this.refresh_insights_status);
this.refresh_insights_status();
}
refresh_insights_status() {
const subfeats = (cockpit.manifests.subscriptions && cockpit.manifests.subscriptions.features) || { };
if (subfeats.insights && this.insights_client_timer.exists && !this.insights_client_timer.enabled)
this.setState({ insightsLinkVisible: true });
else
this.setState({ insightsLinkVisible: false });
}
refresh_os_updates_state() {
const status = page_status.get("updates") || { };
const details = status.details;
this.setState({
updateDetails: details,
updateStatus: status,
});
}
render() {
const pageStatusNotifications = React.createElement(PageStatusNotifications);
const updateDetails = this.state.updateDetails || { };
return (
<Card>
<CardHeader>{_("Health")}</CardHeader>
<CardBody>
<ul className="system-health-events">
<li id="page_status_notifications">{pageStatusNotifications}</li>
<li>
<>
{!!updateDetails.icon && <>
<span id="system_information_updates_icon" className={updateDetails.icon || ""} />
<a id="system_information_updates_text" onClick={() => cockpit.jump("/" + (updateDetails.link || "updates"))}>{updateDetails.text || this.state.updateStatus.title || ""}</a>
</>}
</>
</li>
{this.state.insightsLinkVisible && <li className="system-health-insights">
<span className="pficon pficon-warning-triangle-o" />
{ cockpit.manifests.subscriptions
? <a id="insights_text" tabIndex='0' role="button" onClick={() => cockpit.jump("/subscriptions")}>{_("Not connected to Insights")}</a>
: <span id="insights_text">{_("Not connected to Insights")}</span>}
</li>}
</ul>
</CardBody>
<CardFooter />
</Card>
);
}
}

View File

@ -0,0 +1,36 @@
.system-health {
&-events {
> li {
// Better align system health icons
display: flex;
// Align icons vertically to text
align-items: center;
justify-content: start;
+ li {
margin-top: 0.5rem;
}
.fa,
.pficon {
// Bump up icon size
font-size: 1.25rem;
text-decoration: none;
}
> .fa,
> .pficon {
// Some icons are not the same width; give them a suggested width
flex-basis: 1.25rem;
display: flex;
// Align icons to the center of the basic width
justify-content: center;
}
> :not(a):last-child {
flex: auto;
}
}
}
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 React from 'react';
import $ from "jquery";
import cockpit from "cockpit";
import './motdCard.less';
export class MotdCard extends React.Component {
constructor() {
super();
this.state = { motdVisible: false };
}
componentDidMount() {
const self = this;
cockpit.file("/etc/motd").watch(function(content) {
if (content)
content = content.trimRight();
if (content && content != cockpit.localStorage.getItem('dismissed-motd')) {
self.setState({ motdText: content, motdVisible: true });
} else {
self.setState({ motdVisible: false });
}
// To help the tests known when we have loaded motd
$('#motd-box').attr('data-stable', 'yes');
});
}
render() {
if (!this.state.motdVisible)
return null;
return (
<div id="motd-box" className="motd-box">
<div className="pf-c-alert pf-m-info pf-m-inline" aria-label="Info alert">
<div className="pf-c-alert__icon">
<i className="fa fa-info-circle" aria-hidden="true" />
</div>
<h4 className="pf-c-alert__title">
<pre id="motd">{this.state.motdText}</pre>
</h4>
<div className="pf-c-alert__action">
<button className="pf-c-button pf-m-plain" type="button" onClick={() => {
this.setState({ motdVisible: false });
cockpit.localStorage.setItem('dismissed-motd', $('#motd').text());
}}>
<i className="fa fa-times" aria-hidden="true" />
</button>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,8 @@
#motd {
background-color: transparent;
border: none;
font-size: 14px;
padding: 0px;
margin: 0px;
white-space: pre-wrap;
}

View File

@ -0,0 +1,161 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 * as service from "service.js";
import $ from "jquery";
export function ServerTime() {
var self = this;
var client = cockpit.dbus('org.freedesktop.timedate1');
var timedate = client.proxy();
var time_offset = null;
var remote_offset = null;
this.client = client;
self.timedate = timedate;
this.ntp_waiting_value = null;
this.ntp_waiting_resolve = null;
self.timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
self.timesyncd_service = service.proxy("systemd-timesyncd.service");
/*
* The time we return from here as its UTC time set to the
* server time. This is the only way to get predictable
* behavior and formatting of a Date() object in the absence of
* IntlDateFormat and friends.
*/
Object.defineProperty(self, 'utc_fake_now', {
enumerable: true,
get: function get() {
var offset = time_offset + remote_offset;
return new Date(offset + (new Date()).valueOf());
}
});
Object.defineProperty(self, 'now', {
enumerable: true,
get: function get() {
return new Date(time_offset + (new Date()).valueOf());
}
});
self.format = function format(and_time) {
var string = self.utc_fake_now.toISOString();
if (!and_time)
return string.split('T')[0];
var pos = string.lastIndexOf(':');
if (pos !== -1)
string = string.substring(0, pos);
return string.replace('T', ' ');
};
self.updateInterval = window.setInterval(function() {
$(self).triggerHandler("changed");
}, 30000);
self.wait = function wait() {
if (remote_offset === null)
return self.update();
return cockpit.resolve();
};
self.update = function update() {
return cockpit.spawn(["date", "+%s:%z"], { err: "message" })
.done(function(data) {
const parts = data.trim().split(":");
const timems = parseInt(parts[0], 10) * 1000;
let tzmin = parseInt(parts[1].slice(-2), 10);
const tzhour = parseInt(parts[1].slice(0, -2));
if (tzhour < 0)
tzmin = -tzmin;
const offsetms = (tzhour * 3600000) + tzmin * 60000;
const now = new Date();
time_offset = (timems - now.valueOf());
remote_offset = offsetms;
$(self).triggerHandler("changed");
})
.fail(function(ex) {
console.log("Couldn't calculate server time offset: " + cockpit.message(ex));
});
};
self.change_time = function change_time(datestr, hourstr, minstr) {
var dfd = $.Deferred();
/*
* The browser is brain dead when it comes to dates. But even if
* it wasn't, or we loaded a library like moment.js, there is no
* way to make sense of this date without a round trip to the
* server ... the timezone is really server specific.
*/
cockpit.spawn(["date", "--date=" + datestr + " " + hourstr + ":" + minstr, "+%s"])
.fail(function(ex) {
dfd.reject(ex);
})
.done(function(data) {
var seconds = parseInt(data.trim(), 10);
timedate.call('SetTime', [seconds * 1000 * 1000, false, true])
.fail(function(ex) {
dfd.reject(ex);
})
.done(function() {
self.update();
dfd.resolve();
});
});
return dfd;
};
self.poll_ntp_synchronized = function poll_ntp_synchronized() {
client.call(timedate.path,
"org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTPSynchronized"])
.fail(function(error) {
if (error.name != "org.freedesktop.DBus.Error.UnknownProperty" &&
error.problem != "not-found")
console.log("can't get NTPSynchronized property", error);
})
.done(function(result) {
var ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
var data = { };
data[timedate.path] = ifaces;
client.notify(data);
});
};
self.ntp_updated = function ntp_updated(path, iface, member, args) {
if (!self.ntp_waiting_resolve || !args[1].NTP)
return;
if (self.ntp_waiting_value !== args[1].NTP.v)
console.warn("Unexpected value of NTP");
self.ntp_waiting_resolve();
self.ntp_waiting_resolve = null;
};
self.close = function close() {
client.close();
};
self.update();
}

View File

@ -0,0 +1,120 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 React from 'react';
import { Card, CardHeader, CardBody, CardFooter } from '@patternfly/react-core';
import cockpit from "cockpit";
import * as machine_info from "machine-info.js";
import "./systemInformationCard.less";
const _ = cockpit.gettext;
export class SystemInfomationCard extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.getDMIInfo = this.getDMIInfo.bind(this);
this.getMachineId = this.getMachineId.bind(this);
}
componentDidMount() {
this.getDMIInfo();
this.getMachineId();
}
getMachineId() {
var machine_id = cockpit.file("/etc/machine-id");
var self = this;
machine_id.read()
.done(function(content) {
self.setState({ machineID: content });
})
.fail(function(ex) {
// FIXME show proper Alerts
console.error("Error reading machine id", ex);
})
.always(function() {
machine_id.close();
});
}
getDMIInfo() {
var self = this;
machine_info.dmi_info()
.then(function(fields) {
let vendor = fields.sys_vendor;
let name = fields.product_name;
if (!vendor || !name) {
vendor = fields.board_vendor;
name = fields.board_name;
}
if (!vendor || !name)
self.setState({ hardwareText: undefined });
else
self.setState({ hardwareText: vendor + " " + name });
self.setState({ assetTagText: fields.product_serial || fields.chassis_serial });
}, function(ex) {
// FIXME show proper Alerts
console.debug("couldn't read dmi info: " + ex);
self.setState({ assetTagText: undefined, hardwareText: undefined });
});
}
render() {
return (
<Card className="system-information">
<CardHeader>{_("System information")}</CardHeader>
<CardBody>
<table className="pf-c-table pf-m-grid-md pf-m-compact">
<tbody>
{this.state.hardwareText && <tr>
<th scope="row">{_("Model")}</th>
<td>
<div id="system_information_hardware_text">{this.state.hardwareText}</div>
</td>
</tr>}
{this.state.assetTagText && <tr>
<th scope="row">{_("Asset tag")}</th>
<td>
<div id="system_information_asset_tag_text">{this.state.assetTagText}</div>
</td>
</tr>}
<tr>
<th scope="row" className="system-information-machine-id">{_("Machine ID")}</th>
<td>
<div id="system_machine_id">{this.state.machineID}</div>
</td>
</tr>
</tbody>
</table>
</CardBody>
<CardFooter>
<a className="no-left-padding" onClick={() => cockpit.jump("/system/hwinfo", cockpit.transport.host)}>
{_("View hardware details")}
</a>
</CardFooter>
</Card>
);
}
}

View File

@ -0,0 +1,11 @@
#system_machine_id {
overflow: visible;
white-space: normal;
word-wrap: anywhere;
}
.system-information {
&-machine-id {
white-space: nowrap;
}
}

View File

@ -0,0 +1,99 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 React from 'react';
import {
Card, CardHeader, CardBody, CardFooter,
Progress, ProgressMeasureLocation, ProgressVariant,
} from '@patternfly/react-core';
import * as machine_info from "machine-info.js";
import cockpit from "cockpit";
import "./usageCard.less";
const _ = cockpit.gettext;
const UPDATE_DELAY = 5000;
export class UsageCard extends React.Component {
constructor(props) {
super(props);
this.state = { pollingEnabled: true };
this.updateMemoryInfo = this.updateMemoryInfo.bind(this);
}
componentDidMount() {
this.updateMemoryInfo();
cockpit.addEventListener("visibilitychange", () => {
this.setState((prevState, _) => ({ pollingEnabled: !prevState.pollingEnabled }));
}, () => this.updateMemoryInfo());
}
componentWillUnmount() {
this.setState({ pollingEnabled: false });
}
updateMemoryInfo() {
if (!this.state.pollingEnabled)
return;
machine_info.cpu_ram_info().done(info => {
this.setState({
memTotal: Number((info.memory / (1024 * 1024 * 1024)).toFixed(1)),
memAvailable: Number((info.available_memory / (1024 * 1024 * 1024)).toFixed(1))
});
});
window.setTimeout(this.updateMemoryInfo.bind(this), UPDATE_DELAY);
}
render() {
const memUsed = Number((this.state.memTotal - this.state.memAvailable).toFixed(1));
const fraction = memUsed / this.state.memTotal;
return (
<Card className="system-usage">
<CardHeader>{_("Usage")}</CardHeader>
<CardBody>
<table className="pf-c-table pf-m-grid-md pf-m-compact">
<tbody>
<tr>
<th scope="row">{_("Memory")}</th>
<td>
<Progress value={memUsed}
className="pf-m-sm"
min={0} max={Number(this.state.memTotal)}
variant={fraction > 0.9 ? ProgressVariant.danger : ProgressVariant.info}
label={cockpit.format(_("$0 GiB / $1 GiB"), memUsed, this.state.memTotal)}
measureLocation={ProgressMeasureLocation.outside} />
</td>
</tr>
</tbody>
</table>
</CardBody>
<CardFooter>
<a className="no-left-padding" onClick={() => cockpit.jump("/system/graphs", cockpit.transport.host)}>
{_("View graphs")}
</a>
</CardFooter>
</Card>
);
}
}

View File

@ -0,0 +1,11 @@
.system-usage {
.pf-c-progress {
/* FIXME */
// This is a hacky, simple approach to make usage bars align.
// If the text is too short, there's too much space on the right.
// If the text is too long, it flows over and the bar is smaller.
// It's better than doing nothing, however...
// A more proper fix may require reworking the HTML a bit.
grid-template-columns: 1fr minmax(50%, auto);
}
}

150
pkg/systemd/overview.jsx Normal file
View File

@ -0,0 +1,150 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 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 'polyfills.js';
import cockpit from "cockpit";
import $ from "jquery";
import React from 'react';
import ReactDOM from 'react-dom';
import {
Page, PageSection, PageSectionVariants,
Gallery, Button,
Dropdown, DropdownItem, KebabToggle,
} from '@patternfly/react-core';
import { shutdown, shutdown_modal_setup } from "./shutdown.js";
import { SystemInfomationCard } from './overview-cards/systemInformationCard.jsx';
import { ConfigurationCard } from './overview-cards/configurationCard.jsx';
import { HealthCard } from './overview-cards/healthCard.jsx';
import { MotdCard } from './overview-cards/motdCard.jsx';
import { UsageCard } from './overview-cards/usageCard.jsx';
import { ServerTime } from './overview-cards/serverTime.js';
const _ = cockpit.gettext;
var permission = cockpit.permission({ admin: true });
permission.addEventListener("changed", update_shutdown_privileged);
function update_shutdown_privileged() {
$(".shutdown-privileged").update_privileged(
permission, cockpit.format(
_("The user <b>$0</b> is not permitted to shutdown or restart this server"),
permission.user ? permission.user.name : ''),
'bottom'
);
}
class OverviewPage extends React.Component {
constructor(props) {
super(props);
this.state = {
actionKebabIsOpen: false
};
this.onKebabToggle = actionKebabIsOpen => this.setState({ actionKebabIsOpen });
this.onKebabSelect = event => this.setState({ actionKebabIsOpen: !this.state.actionKebabIsOpen });
this.hostnameMonitor = this.hostnameMonitor.bind(this);
}
componentDidMount() {
this.hostnameMonitor();
shutdown_modal_setup();
}
hostname_text() {
if (!this.state.hostnameData)
return undefined;
const pretty_hostname = this.state.hostnameData.PrettyHostname;
const static_hostname = this.state.hostnameData.StaticHostname;
let str = this.state.hostnameData.HostName;
if (pretty_hostname && static_hostname && static_hostname != pretty_hostname)
str = pretty_hostname + " (" + static_hostname + ")";
else if (static_hostname)
str = static_hostname;
return str || '';
}
hostnameMonitor() {
this.client = cockpit.dbus('org.freedesktop.hostname1',
{ superuser : "try" });
this.hostname_proxy = this.client.proxy('org.freedesktop.hostname1',
'/org/freedesktop/hostname1');
this.hostname_proxy.addEventListener("changed", data => {
this.setState({ hostnameData: data.detail });
});
}
render() {
const { actionKebabIsOpen } = this.state;
const dropdownItems = [
<DropdownItem key="shutdown" onClick={() => shutdown('shutdown', new ServerTime())} component="button">
{_("Shutdown")}
</DropdownItem>,
];
return (
<Page>
<PageSection className='ct-overview-header' variant={PageSectionVariants.light}>
<div className='ct-overview-header-hostname'>
<h1>
{this.hostname_text() || ""}
</h1>
{this.state.hostnameData &&
this.state.hostnameData.OperatingSystemPrettyName &&
<div className="ct-overview-header-subheading" id="system_information_os_text">{cockpit.format(_("running $0"), this.state.hostnameData.OperatingSystemPrettyName)}</div>}
</div>
<div className='ct-overview-header-actions'>
<Button className="shutdown-privileged" id='restart-button' variant="secondary" onClick={() => shutdown('restart', new ServerTime())}>
{_("Restart")}
</Button>
<Dropdown
id="shutdown-group"
className="shutdown-privileged"
position="right"
onSelect={this.onKebabSelect}
toggle={<KebabToggle onToggle={this.onKebabToggle} />}
isOpen={actionKebabIsOpen}
isPlain
dropdownItems={dropdownItems}
/>
</div>
</PageSection>
<PageSection variant={PageSectionVariants.default}>
<Gallery className='ct-system-overview' gutter="lg">
<MotdCard />
<HealthCard />
<UsageCard />
<SystemInfomationCard />
<ConfigurationCard hostname={this.hostname_text()} />
</Gallery>
</PageSection>
</Page>
);
}
}
function init() {
cockpit.translate();
ReactDOM.render(<OverviewPage />, document.getElementById("overview"));
}
document.addEventListener("DOMContentLoaded", init);

248
pkg/systemd/overview.less Normal file
View File

@ -0,0 +1,248 @@
@import "../../node_modules/@patternfly/react-styles/css/components/Table/table.css";
@import "./system-global.less";
/* System Time Modal dialog needs table.css */
@import "../lib/table.css";
#overview {
height: 100%;
}
.ct-overview-header {
align-items: center;
display: flex;
flex-wrap: wrap;
&,
&-hostname {
flex-wrap: wrap;
}
&-actions,
&-hostname {
box-sizing: border-box;
display: flex;
}
&-hostname {
align-items: baseline;
flex: auto;
> h1 {
padding-right: 1rem;
font-size: var(--pf-global--FontSize--2xl);
}
}
&-actions {
align-items: center;
}
&-subheading {
font-size: var(--pf-global--FontSize--xl);
}
}
.ct-system-overview {
--card-width: 24rem;
--pf-l-gallery--GridTemplateColumns: repeat(auto-fill, minmax(var(--card-width), 1fr));
.motd-box {
grid-column: ~"1 / -1";
}
.pf-c-card {
&__header {
font-size: var(--pf-global--FontSize--2xl);
font-weight: var(--pf-global--FontWeight--normal);
}
&__body {
.fa,
.pficon {
+ a {
/* Space out icons + links */
margin-left: 0.5rem;
}
}
a {
> .fa,
> .pficon {
/* Space out icons inside of links */
margin-right: 0.5rem;
}
}
&:last-child .pf-c-table:last-child tr:last-child {
/* Remove the border of tables when it's the last item in a card and there isn't a card footer */
border-bottom: none;
}
p {
+ p,
+ button {
margin-top: calc(var(--pf-global--LineHeight--md) * 1rem);
}
}
td {
vertical-align: middle;
}
th {
font-size: var(--pf-global--FontSize--sm);
}
}
&__footer {
&:empty {
display: none;
}
}
}
.pf-c-progress {
&__status {
display: flex;
align-items: baseline;
&-icon {
display: flex;
align-self: center;
}
}
}
.pf-m-compact {
th, td {
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
}
}
@media (max-width: 320px) {
/* Make the overview fit on very narrow screens like an iPhone SE */
.pf-c-page__main-section:not(.ct-overview-header) {
/* Remove left and right padding for cards on narrow viewports */
--pf-c-page__main-section--PaddingRight: 0;
--pf-c-page__main-section--PaddingLeft: 0;
--pf-c-page__main-section--PaddingTop: 0.5rem;
--pf-c-page__main-section--PaddingBottom: 0.5rem;
}
.ct-system-overview {
/* Reduce spacing between items */
--pf-l-gallery--m-gutter--GridGap: 0.25rem;
}
}
@media (max-width: 640px) {
.ct-system-overview {
/* Allow cards to be narrower on very narrow viewports */
--card-width: 15rem;
}
}
#machine_id {
font-family: var(--pf-global--FontFamily--redhatfont--monospace);
word-wrap: anywhere;
}
.pf-c-table tr > * {
vertical-align: top;
}
.ct-inline-list .pf-c-list.pf-m-inline {
display: inline-flex;
margin-left: 1rem;
}
.realms-op-diagnostics {
/* standard PF alerts have a wide margin */
max-width: 550px;
text-align: left;
max-height: 200px;
}
.realms-op-error .realms-op-more-diagnostics {
font-weight: normal;
}
.realms-op-leave-only-row .pf-c-alert {
/* standard PF alerts have a wide margin */
padding-left: 2ex;
button {
margin: 1ex 0;
}
}
#realms-op-leave-toggle {
font-weight: bold;
/* leave some space between form and leave toggle */
line-height: 5rem;
}
.realms-op-wait-message {
margin-left: 10px;
float: left;
margin-top: 3px;
}
#sich-note-1,
#sich-note-2 {
margin: 0;
}
.small-messages {
font-size: smaller;
}
.systime-inline {
.form-control {
margin: 0 4px;
}
.form-group {
&:first-of-type .form-control {
margin: 0 4px 0 0;
}
.form-control {
width: 214px;
}
}
.form-inline {
background: #f4f4f4;
border: 1px solid #bababa;
padding: 4px;
&:not(:first-of-type) {
border-top-width: 0;
}
}
form {
.pficon-close,
.fa-plus {
float: right;
margin-left: 5px;
padding: 4px;
height: 26px;
width: 26px;
}
}
}
.pf-c-button.no-left-padding {
padding-left: 0;
}

View File

@ -51,14 +51,15 @@ export class PageStatusNotifications extends React.Component {
const page = "system/services";
const status = page_status.get(page);
if (status && status.type && status.title) {
this.props.toggle_label(true);
this.props.toggle_label && this.props.toggle_label(true);
const jump = () => cockpit.jump("/" + page);
return (
<span>
<span className={icon_class_for_type(status.type)} /> <button className="link-button" role="link" onClick={jump}>{status.title}</button>
</span>);
<>
<span className={icon_class_for_type(status.type)} />
<a role="button" tabIndex="0" onClick={jump}>{status.title}</a>
</>);
} else {
this.props.toggle_label(false);
this.props.toggle_label && this.props.toggle_label(false);
return null;
}
}

View File

@ -26,7 +26,7 @@ import { Button, Modal, OverlayTrigger, Tooltip, DropdownKebab, MenuItem } from
import cockpit from "cockpit";
import { OnOffSwitch } from "cockpit-components-onoff.jsx";
import './service-details.css';
import './service-details.less';
const _ = cockpit.gettext;

View File

@ -1,7 +1,7 @@
@import "./timer.css";
@import "./timer.less";
@import '../lib/journal.css';
@import "../lib/table.css";
@import "./system-global.css";
@import "./system-global.less";
#services-page {
overflow-y: scroll;
@ -213,8 +213,12 @@ table.systemd-unit-relationship-table td:first-child {
width: 40%;
}
.fa-exclamation-triangle {
color: var(--pf-global--warning-color--100);
.service-template input {
width: 50em;
}
.service-unit-failed {
color: red;
}
.service-template input {

View File

@ -24,7 +24,7 @@ import cockpit from "cockpit";
import "patterns";
import "bootstrap-datepicker/dist/js/bootstrap-datepicker";
import "./shutdown.css";
import "./shutdown.less";
const _ = cockpit.gettext;
@ -34,6 +34,9 @@ var server_time = null;
/* The current operation */
var operation = null;
/* The delay in the dialog */
var delay = 0;
/* The entry point, shows the dialog */
export function shutdown(op, st) {
operation = op;
@ -41,65 +44,71 @@ export function shutdown(op, st) {
$('#shutdown-dialog').modal('show');
}
$('#shutdown-dialog .shutdown-date').datepicker({
autoclose: true,
todayHighlight: true,
format: 'yyyy-mm-dd',
startDate: "today",
});
$("#shutdown-dialog input")
.on('focusout', update)
.on('change', update);
/* The delay in the dialog */
var delay = 0;
$("#shutdown-dialog .dropdown li")
.on("click", function(ev) {
delay = $(this).attr("value");
update();
});
/* Prefilling the date if it's been set */
var cached_date = null;
$('#shutdown-dialog .shutdown-date')
.on('focusin', function() {
cached_date = $(this).val();
})
.on('focusout', function() {
if ($(this).val().length === 0)
$(this).val(cached_date);
});
$("#shutdown-dialog").on("show.bs.modal", function(ev) {
/* The date picker also triggers this event, since it is modal */
if (ev.target.id !== "shutdown-dialog")
return;
$("#shutdown-dialog textarea")
.val("")
.attr("placeholder", _("Message to logged in users"))
.attr("rows", 5);
/* Track the value correctly */
delay = $("#shutdown-dialog li:first-child").attr("value");
server_time.wait().then(function() {
$('#shutdown-dialog .shutdown-date').val(server_time.format());
$('#shutdown-dialog .shutdown-hours').val(server_time.utc_fake_now.getUTCHours());
$('#shutdown-dialog .shutdown-minutes').val(server_time.utc_fake_now.getUTCMinutes());
export function shutdown_modal_setup() {
$('#shutdown-dialog .shutdown-date').datepicker({
autoclose: true,
todayHighlight: true,
format: 'yyyy-mm-dd',
startDate: "today",
});
if (operation == 'shutdown') {
$('#shutdown-dialog .modal-title').text(_("Shut Down"));
$("#shutdown-dialog .btn-danger").text(_("Shut Down"));
} else {
$('#shutdown-dialog .modal-title').text(_("Restart"));
$("#shutdown-dialog .btn-danger").text(_("Restart"));
}
$("#shutdown-dialog input")
.on('focusout', update)
.on('change', update);
update();
});
$("#shutdown-dialog .dropdown li")
.on("click", function(ev) {
delay = $(this).attr("value");
update();
});
/* Prefilling the date if it's been set */
var cached_date = null;
$('#shutdown-dialog .shutdown-date')
.on('focusin', function() {
cached_date = $(this).val();
})
.on('focusout', function() {
if ($(this).val().length === 0)
$(this).val(cached_date);
});
$("#shutdown-dialog").on("show.bs.modal", function(ev) {
/* The date picker also triggers this event, since it is modal */
if (ev.target.id !== "shutdown-dialog")
return;
$("#shutdown-dialog textarea")
.val("")
.attr("placeholder", _("Message to logged in users"))
.attr("rows", 5);
/* Track the value correctly */
delay = $("#shutdown-dialog li:first-child").attr("value");
server_time.wait().then(function() {
$('#shutdown-dialog .shutdown-date').val(server_time.format());
$('#shutdown-dialog .shutdown-hours').val(server_time.utc_fake_now.getUTCHours());
$('#shutdown-dialog .shutdown-minutes').val(server_time.utc_fake_now.getUTCMinutes());
});
if (operation == 'shutdown') {
$('#shutdown-dialog .modal-title').text(_("Shut Down"));
$("#shutdown-dialog .btn-danger").text(_("Shut Down"));
} else {
$('#shutdown-dialog .modal-title').text(_("Restart"));
$("#shutdown-dialog .btn-danger").text(_("Restart"));
}
update();
});
/* Perform the action */
$("#shutdown-dialog .btn-danger").click(function() {
$("#shutdown-dialog").dialog("promise", perform());
});
}
function update() {
$("#shutdown-dialog input")

View File

@ -1,4 +1,5 @@
@import "../lib/page.css";
@import '../lib/form-layout.less';
@import "../../node_modules/@patternfly/react-styles/css/components/Alert/alert.css";
.pf-c-alert {

View File

@ -30,10 +30,10 @@ class TestHWinfo(SeleniumTest):
self.machine.execute(command=cmd, input=self.lscpu)
self.machine.execute('sudo chmod a+x {}'.format(self.lscpu_file))
self.login()
self.click(self.wait_link('System', cond=clickable))
self.click(self.wait_link('Overview', cond=clickable))
self.wait_frame("localhost/system")
self.click(self.wait_id("system_information_hardware_text"))
self.click(self.wait_link("View hardware details"))
self.mainframe()
self.wait_frame("localhost/system/hwinfo")
self.wait_id("hwinfo")

View File

@ -18,13 +18,13 @@ class NavigateTestSuite(SeleniumTest):
def testNavigateNoReload(self):
self.login()
# Bring up a dialog on system page
self.click(self.wait_link('System', cond=clickable))
self.click(self.wait_link('Overview', cond=clickable))
self.wait_frame("system")
self.click(self.wait_id('system_information_systime_button', cond=clickable))
self.wait_id('system_information_change_systime', cond=visible)
# Check hardware info page
self.click(self.wait_id('system_information_hardware_text', cond=clickable))
self.click(self.wait_link('View hardware details', cond=clickable))
self.mainframe()
self.wait_frame("hwinfo")
self.wait_text('BIOS date')
@ -37,6 +37,6 @@ class NavigateTestSuite(SeleniumTest):
self.mainframe()
# Now navigate back to system page
self.click(self.wait_link('System', cond=clickable))
self.click(self.wait_link('Overview', cond=clickable))
self.wait_frame("system")
self.wait_id('system_information_change_systime', cond=visible)

View File

@ -26,14 +26,14 @@ class TunedProfiles(SeleniumTest):
return self.machine.execute("/usr/sbin/tuned-adm active", quiet=True).strip().rsplit(" ", 1)[1]
def testPerformaceProfiles(self):
self.click(self.wait_link('System', cond=clickable))
self.click(self.wait_link('Overview', cond=clickable))
self.wait_frame("system")
self.click(self.wait_text(self.balanced_profile, cond=clickable))
self.wait_text("Change Performance Profile")
self.click(self.wait_text(self.desktop_profile, element="p", cond=clickable))
self.click(self.wait_text("Change Profile", element="button", cond=clickable))
self.wait_text("Change Performance Profile", cond=invisible)
self.wait_id("server", cond=visible)
self.wait_id("overview", cond=visible)
self.wait_text(self.desktop_profile, cond=clickable)
self.assertIn(self.desktop_profile, self.get_profile())
@ -42,6 +42,6 @@ class TunedProfiles(SeleniumTest):
self.click(self.wait_text(self.balanced_profile, element="p", cond=clickable))
self.click(self.wait_text("Change Profile", element="button", cond=clickable))
self.wait_text("Change Performance Profile", cond=invisible)
self.wait_id("server", cond=visible)
self.wait_id("overview", cond=visible)
self.wait_text(self.balanced_profile, cond=clickable)
self.assertIn(self.balanced_profile, self.get_profile())

View File

@ -39,7 +39,10 @@ class TestActivePages(MachineCase):
# /playground/preloaded, /system/services, and /updates
n_extra_preloaded = 3
b.wait_present("#server")
if m.image in [ "rhel-8-1-distropkg" ]:
b.wait_present("#server")
else:
b.wait_present("#overview")
def showPagesAssertCount(count):
b.switch_to_top()

View File

@ -521,8 +521,11 @@ class TestConnection(MachineCase):
m.wait_for_cockpit_running('127.0.0.90', 9999)
# System frame should work directly, no login page
out = m.execute("curl --compressed http://127.0.0.90:9999/cockpit/@localhost/system/index.html")
self.assertIn('id="system_machine_id"', out)
self.assertIn('data-action="shutdown"', out)
if m.image == "rhel-8-1-distropkg":
self.assertIn('id="system_machine_id"', out)
self.assertIn('data-action="shutdown"', out)
else:
self.assertIn('id="overview"', out)
# shut it down, wait until it is gone
m.execute("pkill -ef cockpit-ws")
@ -539,16 +542,21 @@ G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 --local
# System frame should work directly, no login page
out = m.execute("curl --compressed http://127.0.0.90:9999/cockpit/@localhost/system/index.html")
self.assertIn('id="system_machine_id"', out)
self.assertIn('data-action="shutdown"', out)
if m.image == "rhel-8-1-distropkg":
self.assertIn('id="system_machine_id"', out)
self.assertIn('data-action="shutdown"', out)
else:
self.assertIn('id="overview"', out)
self.allow_journal_messages("couldn't register polkit authentication agent.*")
@skipImage("OSTree doesn't have cockpit-ws", "fedora-coreos")
@skipImage("Kernel does not allow user namespaces", "debian-stable", "debian-testing")
def testCockpitDesktop(self):
m = self.machine
cases = [(['/cockpit/@localhost/system/index.html', 'system', 'system/index', 'system/'],
['id="system_machine_id"', 'data-action="shutdown"']
['id="system_machine_id"' if m.image == "rhel-8-1-distropkg" else 'id="overview"']
),
(['/cockpit/@localhost/network/firewall.html', 'network/firewall'],
['div id="firewall"', 'script src="firewall.js"']
@ -558,8 +566,6 @@ G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 --local
),
]
m = self.machine
if "debian" in m.image or "ubuntu" in m.image:
cockpit_desktop = "/usr/lib/cockpit/cockpit-desktop"
else:

View File

@ -212,6 +212,9 @@ class TestBasicDashboard(MachineCase, DashBoardHelpers):
b.enter_page("/system", "10.111.113.3")
b.wait_text_not("#system_information_systime_button", "")
if m.image not in ["rhel-8-1-distropkg"]:
b.click(".system-usage a") # View graphs
b.enter_page("/system/graphs", "10.111.113.3")
b.click("#link-network a")
b.enter_page("/network", "10.111.113.3")

View File

@ -375,9 +375,13 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
m.start_cockpit()
b.login_and_go("/system", user="unpriv")
# not an admin
b.wait_present("#shutdown-group button[data-action=restart][data-stable=yes]")
b.wait_present("#shutdown-group button.disabled")
b.wait_not_present("#shutdown-group button:not(.disabled)")
if m.image == "rhel-8-1-distropkg":
b.wait_present("#shutdown-group button[data-action=restart][data-stable=yes]")
b.wait_present("#shutdown-group button.disabled")
b.wait_not_present("#shutdown-group button:not(.disabled)")
else:
b.wait_present("#restart-button.disabled[data-stable=yes]")
b.wait_present("#shutdown-group.disabled[data-stable=yes]")
b.logout()
self.allow_authorize_journal_messages()
# not allowed to restricted users
@ -396,7 +400,10 @@ account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group
m.execute("semanage login -a -s sysadm_u admin")
b.login_and_go("/system")
# shutdown button should be enabled and working
b.click("#shutdown-group button[data-action=restart][data-stable=yes]")
if m.image == "rhel-8-1-distropkg":
b.click("#shutdown-group button[data-action=restart][data-stable=yes]")
else:
b.click("#restart-button[data-stable=yes]")
b.wait_popup("shutdown-dialog")
b.wait_in_text("#shutdown-dialog .btn-danger", 'Restart')
b.click('#shutdown-dialog button[data-dismiss="modal"]')

View File

@ -147,11 +147,17 @@ class TestMultiMachineAdd(MachineCase):
def testBasic(self):
b = self.browser
m = self.machine
m2 = self.machine2
m3 = self.machine3
m3_host = "10.111.113.3:2222"
change_ssh_port(m3, "10.111.113.3", 2222)
if m.image == "rhel-8-1-distropkg":
hostname_selector = "#system_information_hostname_button"
else:
hostname_selector = "#system_information_hostname_text"
self.login_and_go(None)
add_machine(b, "10.111.113.2")
add_machine(b, m3_host)
@ -176,7 +182,7 @@ class TestMultiMachineAdd(MachineCase):
b.switch_to_top()
b.wait_js_cond('window.location.pathname != "/dashboard"')
b.enter_page("/system", host="10.111.113.2")
b.wait_text_not("#system_information_hostname_button", "")
b.wait_text_not(hostname_selector, "")
b.switch_to_top()
b.go("/dashboard")
b.enter_page("/dashboard")
@ -186,7 +192,7 @@ class TestMultiMachineAdd(MachineCase):
b.switch_to_top()
b.wait_js_cond('window.location.pathname != "/dashboard"')
b.enter_page("/system", host=m3_host)
b.wait_text_not("#system_information_hostname_button", "")
b.wait_text_not(hostname_selector, "")
b.switch_to_top()
b.go("/dashboard")
b.enter_page("/dashboard")
@ -243,6 +249,11 @@ class TestMultiMachine(MachineCase):
name = self.machine.execute("hostname")
if m.image == "rhel-8-1-distropkg":
hostname_selector = "#system_information_hostname_button"
else:
hostname_selector = "#system_information_hostname_text"
# Change os-release pretty name on m2
m2.execute("sed -i '/NAME=.*/d' /etc/os-release")
m2.execute("echo 'NAME=\"A Pretty Name\"' >> /etc/os-release")
@ -250,7 +261,7 @@ class TestMultiMachine(MachineCase):
b.switch_to_top()
b.go("{}/system".format(root))
b.enter_page("/system")
b.wait_in_text('#system_information_hostname_button', name.strip())
b.wait_in_text(hostname_selector, name.strip())
b.switch_to_top()
b.wait_js_cond('window.location.pathname == "{0}system"'.format(root))
b.click("a[href='/dashboard']")
@ -271,7 +282,7 @@ class TestMultiMachine(MachineCase):
b.click('#login-button')
b.expect_load()
b.enter_page("/system")
b.wait_in_text('#system_information_hostname_button', "machine2")
b.wait_in_text(hostname_selector, "machine2")
b.switch_to_top()
# Branding uses m2 pretty name
@ -367,7 +378,7 @@ class TestMultiMachine(MachineCase):
b.click('#login-button')
b.expect_load()
b.enter_page("/system")
b.wait_in_text('#system_information_hostname_button', "machine2")
b.wait_in_text(hostname_selector, "machine2")
b.logout()
# Check hostkey isn't saved
@ -421,7 +432,7 @@ class TestMultiMachine(MachineCase):
b.click('#login-button')
b.expect_load()
b.enter_page("/system")
b.wait_in_text('#system_information_hostname_button', "machine2")
b.wait_in_text(hostname_selector, "machine2")
b.switch_to_top()
b.wait_js_cond('window.location.pathname == "{0}=10.111.113.2/system"'.format(root))
@ -464,6 +475,11 @@ class TestMultiMachine(MachineCase):
b = self.browser
m = self.machine
if m.image == "rhel-8-1-distropkg":
hostname_selector = "#system_information_hostname_button"
else:
hostname_selector = "#system_information_hostname_text"
m.execute('mkdir -p /etc/cockpit/ && echo "[WebService]\nUrlRoot = cockpit-new" > /etc/cockpit/cockpit.conf')
m.start_cockpit()
@ -499,7 +515,7 @@ class TestMultiMachine(MachineCase):
b.enter_page("/dashboard")
add_machine(b, "10.111.113.2")
b.enter_page("/system", host="10.111.113.2")
b.wait_text("#system_information_hostname_button", "machine2")
b.wait_text(hostname_selector, "machine2")
b.switch_to_top()
b.wait_js_cond('window.location.pathname == "/cockpit-new/@10.111.113.2/system"')

View File

@ -166,7 +166,10 @@ class TestUpdates(PackageCase):
b.wait_text("#system_information_updates_text", "Bug Fix Updates Available")
self.assertIn("fa-bug", b.attr("#system_information_updates_icon", "class"))
# should be a link, click on it to go to /updates
b.click("#system_information_updates_text a")
if m.image == "rhel-8-1-distropkg":
b.click("#system_information_updates_text a")
else:
b.click("#system_information_updates_text")
b.enter_page("/updates")
# old versions are still installed
@ -329,7 +332,10 @@ class TestUpdates(PackageCase):
self.assertIn("security", b.attr("#system_information_updates_icon", "class"))
# should be a link, click on it to go to back to /updates
b.click("#system_information_updates_text a")
if m.image == "rhel-8-1-distropkg":
b.click("#system_information_updates_text a")
else:
b.click("#system_information_updates_text")
b.enter_page("/updates")
# install only security updates
@ -737,7 +743,10 @@ class TestUpdatesSubscriptions(PackageCase):
b.wait_in_text("#system_information_updates_text", "Not Registered")
self.assertIn("triangle", b.attr("#system_information_updates_icon", "class"))
# should be a link leading to subscriptions page
b.click("#system_information_updates_text a")
if m.image == "rhel-8-1-distropkg":
b.click("#system_information_updates_text a")
else:
b.click("#system_information_updates_text")
b.enter_page("/subscriptions")
# software updates page also shows unregistered

View File

@ -82,7 +82,7 @@ OnCalendar=daily
b.switch_to_top()
b.click("a[href='/system']")
b.enter_page("/system")
b.wait_present("#system_information_hostname_button")
b.wait_present("#system_information_systime_button")
b.switch_to_top()
b.click("a[href='/system/services']")
b.enter_page("/system/services")
@ -150,7 +150,10 @@ OnCalendar=daily
# Check that the system page is translated
b.go("/system")
b.enter_page("/system")
b.wait_in_text("#server", "Neustarten")
if m.image in ["rhel-8-1-distropkg"]:
b.wait_in_text("#server", "Neustarten")
else:
b.wait_in_text(".ct-overview-header", "Neustarten")
# Systemd timer localization
b.go("/system/services")
@ -219,7 +222,7 @@ OnCalendar=daily
pages = ["/system", "/system/logs", "/network", "/users", "/system/services", "/system/terminal"]
self.login_and_go('/system')
b.wait_present('#server')
b.wait_present('#overview')
b.switch_to_top()
b.click('#content-user-name')
@ -285,7 +288,10 @@ OnCalendar=daily
m.execute('locale-gen pt_BR && locale-gen pt_BR.UTF-8 && update-locale')
self.login_and_go('/system')
b.wait_present('#server')
if m.image in ["rhel-8-1-distropkg"]:
b.wait_present('#server')
else:
b.wait_present('#overview')
b.switch_to_top()
b.click('#content-user-name')
b.click('.display-language-menu a')
@ -300,7 +306,10 @@ OnCalendar=daily
# Check that the system page is translated
b.go('/system')
b.enter_page('/system')
b.wait_in_text('#server', 'Reiniciar')
if m.image in ["rhel-8-1-distropkg"]:
b.wait_in_text('#server', 'Reiniciar')
else:
b.wait_in_text('.ct-overview-header', 'Reiniciar')
# Systemd timer localization
b.go('/system/services')
@ -378,7 +387,10 @@ OnCalendar=daily
self.login_and_go()
b.wait_in_text("#host-apps", "System")
if m.image in ["rhel-8-1-distropkg"]: # Changed in #12265
b.wait_in_text("#host-apps", "System")
else:
b.wait_in_text("#host-apps", "Overview")
m.execute("mkdir -p /home/admin/.local/share/cockpit/foo")
m.write("/home/admin/.local/share/cockpit/foo/manifest.json",
'{ "menu": { "index": { "label": "FOO!" } } }')
@ -412,7 +424,7 @@ OnCalendar=daily
# Check that any substring work
b.focus("#filter-menus")
b.set_val("#filter-menus", "CoUN")
b.wait_not_present("#sidebar-menu > li > a > span:contains('System')")
b.wait_not_present("#sidebar-menu > li > a > span:contains('Overview')")
b.wait_present("#sidebar-menu > li > a > span:contains('Accounts')")
b.wait_text("#sidebar-menu > li > a > span:contains('Accounts') mark", "coun")
@ -472,7 +484,7 @@ OnCalendar=daily
b.focus("#filter-menus")
b.set_val("#filter-menus", "firew")
b.wait_present("#sidebar-menu > li > a > span:contains('Networking')")
b.wait_not_present("#sidebar-menu > li > a > span:contains('System')")
b.wait_not_present("#sidebar-menu > li > a > span:contains('Overview')")
b.click("#sidebar-menu > li > a > span:contains('Networking')")
b.enter_page("/network/firewall")
@ -489,7 +501,7 @@ OnCalendar=daily
b.wait_present("#content")
b.go("/system")
b.enter_page("/system")
b.wait_in_text("#server", "Neustarten")
b.wait_in_text(".ct-overview-header", "Neustarten")
b.switch_to_top()
b.wait_present("#sidebar-menu > li > a > span:contains('Dienste')")

View File

@ -121,9 +121,14 @@ class TestRealms(MachineCase):
# when joined to a domain, changing the hostname is fatal, so should be disabled
b.wait_present("#system_information_hostname_button[disabled]")
b.mouse("#system_information_hostname_button", "mouseover")
b.wait_in_text(".tooltip-inner", "Host name should not be changed in a domain")
b.mouse("#system_information_hostname_button", "mouseout")
if m.image in ["rhel-8-1-distropkg"]:
b.mouse("#system_information_hostname_button", "mouseover")
b.wait_in_text(".tooltip-inner", "Host name should not be changed in a domain")
b.mouse("#system_information_hostname_button", "mouseout")
else:
b.mouse("#system_information_hostname_tooltip", "mouseover")
b.wait_in_text(".tooltip-inner", "Host name should not be changed in a domain")
b.mouse("#system_information_hostname_tooltip", "mouseout")
b.wait_not_present(".tooltip-inner")
# should not have any leftover tickets from the joining
@ -326,7 +331,11 @@ class TestRealms(MachineCase):
b.enter_page('/system')
# shutdown button should be enabled and working
# it takes a while for the permission check to finish, it is always enabled at first
b.click("#shutdown-group button[data-action=restart][data-stable=yes]")
if m.image in ["rhel-8-1-distropkg"]:
b.click("#shutdown-group button[data-action=restart][data-stable=yes]")
else:
b.click("#overview #restart-button")
b.wait_popup("shutdown-dialog")
b.wait_in_text("#shutdown-dialog .btn-danger", 'Restart')
b.click("#shutdown-dialog .dropdown button")

View File

@ -58,9 +58,13 @@ class TestShutdownRestart(MachineCase):
b.click('#login-button')
b.expect_load()
b.enter_page("/system")
b.wait_present("#shutdown-group button[data-action=restart].disabled")
b.wait_present("#shutdown-group button.dropdown-toggle.disabled")
b.wait_not_present("#shutdown-group button:not(.disabled)")
if m.image == "rhel-8-1-distropkg":
b.wait_present("#shutdown-group button[data-action=restart].disabled")
b.wait_present("#shutdown-group button.dropdown-toggle.disabled")
b.wait_not_present("#shutdown-group button:not(.disabled)")
else:
b.wait_present("#restart-button.disabled")
b.wait_present("#restart-button + div.pf-c-dropdown.disabled")
b.logout()
@ -72,7 +76,10 @@ class TestShutdownRestart(MachineCase):
time.sleep(2)
# shutdown button should be enabled and working
b.click("#shutdown-group button[data-action=restart][data-stable=yes]")
if m.image == "rhel-8-1-distropkg":
b.click("#shutdown-group button[data-action=restart][data-stable=yes]")
else:
b.click("#restart-button")
b.wait_popup("shutdown-dialog")
b.wait_in_text("#shutdown-dialog .btn-danger", 'Restart')
b.click("#shutdown-dialog .dropdown button")
@ -96,7 +103,10 @@ class TestShutdownRestart(MachineCase):
self.login_and_go("/system")
# Reboot
b.click("#shutdown-group button[data-action=restart]")
if m.image == "rhel-8-1-distropkg":
b.click("#shutdown-group button[data-action=restart][data-stable=yes]")
else:
b.click("#restart-button")
b.wait_popup("shutdown-dialog")
b.wait_in_text("#shutdown-dialog .btn-danger", 'Restart')
b.click("#shutdown-dialog .dropdown button")
@ -132,10 +142,16 @@ class TestShutdownRestart(MachineCase):
b2.click('#troubleshoot-dialog .btn-primary')
b2.wait_popdown('troubleshoot-dialog')
b2.enter_page("/system", host="10.111.113.1")
b2.wait_text("#system_information_hostname_button", "machine1")
if m.image == "rhel-8-1-distropkg":
b2.wait_text("#system_information_hostname_button", "machine1")
else:
b2.wait_text("#system_information_hostname_text", "machine1")
# Check auto reconnect on restart
b2.click("#shutdown-group button[data-action=restart][data-stable=yes]")
if m.image == "rhel-8-1-distropkg":
b2.click("#shutdown-group button[data-action=restart][data-stable=yes]")
else:
b2.click("#restart-button")
b2.wait_popup("shutdown-dialog")
b2.wait_in_text("#shutdown-dialog .btn-danger", 'Restart')
b2.click("#shutdown-dialog .dropdown button")
@ -157,9 +173,10 @@ class TestShutdownRestart(MachineCase):
b.enter_page("/system")
if m.image == "rhel-8-1-distropkg":
b.click("#shutdown-group span.caret")
b.click("#shutdown-group a:contains('Shut Down')")
else:
b.click("#shutdown-group i.fa-caret-down")
b.click("#shutdown-group a:contains('Shut Down')")
b.click("#shutdown-group .pf-c-dropdown__toggle") # kebab
b.click("#shutdown-group button:contains('Shutdown')")
b.wait_popup("shutdown-dialog")
b.click("#shutdown-dialog .dropdown button")
b.click("a:contains('Specific Time')")

View File

@ -193,11 +193,14 @@ class TestSystemInfo(MachineCase):
b.wait_not_in_text("#system_information_ssh_keys .list-group", old_alt)
b.wait_in_text("#system_information_ssh_keys .list-group", new_alt)
b.wait_text('#system_information_os_text',
b.wait_in_text('#system_information_os_text',
"Foobar Adventure Linux Server 2.0 (Day of Doom)")
m.execute("hostnamectl set-hostname --static --pretty 'Adventure Box'")
b.wait_in_text('#system_information_hostname_button', "Adventure Box")
if m.image == "rhel-8-1-distropkg":
b.wait_in_text('#system_information_hostname_button', "Adventure Box")
else:
b.wait_in_text('#system_information_hostname_text', "Adventure Box")
b.click('#system_information_hostname_button')
b.wait_popup("system_information_change_hostname")
@ -206,13 +209,10 @@ class TestSystemInfo(MachineCase):
b.click("#system_information_change_hostname button:contains('Change')")
b.wait_popdown("system_information_change_hostname")
if m.image in ["fedora-coreos"]:
# rpm-ostree version looks like 30.20191014.0, we don't check out a custom OSTree on the image
b.wait_in_text('#system-ostree-version-link', ".20")
if m.image == "rhel-8-1-distropkg":
b.wait_in_text('#system_information_hostname_button', "Adventure Box (host1.cockpit.lan)")
else:
b.wait_not_visible("#system-ostree-version-link")
b.wait_text('#system_information_hostname_button', "Adventure Box (host1.cockpit.lan)")
b.wait_in_text('#system_information_hostname_text', "Adventure Box (host1.cockpit.lan)")
self.assertEqual(m.execute("hostname").strip(), "host1.cockpit.lan")
b.logout()
@ -353,8 +353,10 @@ class TestSystemInfo(MachineCase):
m.execute("rm -f /etc/motd")
self.login_and_go("/system")
b.wait_present('#motd-box[data-stable=yes]')
b.wait_not_visible('#motd-box')
if m.image == "rhel-8-1-distropkg":
b.wait_not_visible('#motd-box')
else:
b.wait_not_present('#motd-box')
m.execute(r"printf ' Hello\n World\n\n' >/etc/motd")
b.wait_visible('#motd-box')
@ -368,13 +370,19 @@ class TestSystemInfo(MachineCase):
b.click('#motd-box button.close')
else:
b.click('#motd-box button.pf-c-button')
b.wait_not_visible('#motd-box')
if m.image == "rhel-8-1-distropkg":
b.wait_not_visible('#motd-box')
else:
b.wait_not_present('#motd-box')
# motd should stay dismissed after a reload
b.reload()
b.enter_page("/system")
b.wait_present('#motd-box[data-stable=yes]')
b.wait_not_visible('#motd-box')
if m.image == "rhel-8-1-distropkg":
b.wait_not_visible('#motd-box')
else:
b.wait_not_present('#motd-box')
m.execute("echo Hello again >/etc/motd")
b.wait_visible('#motd-box')
@ -389,7 +397,12 @@ class TestSystemInfo(MachineCase):
self.login_and_go("/system")
b.wait_in_text('#system_information_hardware_text', "QEMU")
b.click('#system_information_hardware_text')
if m.image == "rhel-8-1-distropkg":
hardware_page_link = '#system_information_hardware_text'
else:
hardware_page_link = '.system-information a'
b.click(hardware_page_link)
b.enter_page("/system/hwinfo")
# system info
@ -451,12 +464,16 @@ class TestSystemInfo(MachineCase):
b.logout()
self.machine.execute("mount -t tmpfs none /sys/class/dmi/id")
self.login_and_go("/system")
b.wait_present('#system_information_hardware_text')
# asset tag should be hidden
b.wait_not_visible('#system_information_asset_tag_text')
# Hardware can just be a generic link
b.wait_text('#system_information_hardware_text', "Details")
b.click("#system_information_hardware_text")
if m.image == "rhel-8-1-distropkg":
b.wait_not_visible('#system_information_asset_tag_text')
# Hardware can just be a generic link
b.wait_text('#system_information_hardware_text', "Details")
else:
b.wait_not_present('#system_information_asset_tag_text')
# Hardware should be hidden
b.wait_not_present('#system_information_hardware_text')
b.click(hardware_page_link)
b.enter_page("/system/hwinfo")
# CPU should still be shown, but not the DMI fields
@ -475,14 +492,17 @@ class TestSystemInfo(MachineCase):
m.write("/sys/class/dmi/id/chassis_type", "10")
b.go("/system")
b.enter_page('/system')
b.wait_text('#system_information_hardware_text', "Details")
if m.image == "rhel-8-1-distropkg":
b.wait_text('#system_information_hardware_text', "Details")
else:
b.wait_not_present('#system_information_hardware_text')
m.write("/sys/class/dmi/id/board_vendor", "VENDOR")
m.write("/sys/class/dmi/id/board_name", "NAME")
b.reload()
b.enter_page('/system')
b.wait_in_text('#system_information_hardware_text', "VENDOR NAME")
b.click("#system_information_hardware_text")
b.click(hardware_page_link)
b.enter_page("/system/hwinfo")
b.wait_in_text('#hwinfo .info-table-ct tbody:nth-of-type(1) tr:nth-of-type(2) td', "NAME")
b.wait_in_text('#hwinfo .info-table-ct tbody:nth-of-type(1) tr:nth-of-type(3) td', "VENDOR")
@ -718,7 +738,7 @@ fi
b.wait_text("#insights_text", "Not connected to Insights")
m.execute("systemctl enable insights-client.timer")
b.wait_not_visible("#insights_text")
b.wait_not_present("#insights_text")
class TestPcp(packagelib.PackageCase):
@ -730,8 +750,8 @@ class TestPcp(packagelib.PackageCase):
# the OSTree images don't have pcp and can't install additional software
if m.ostree_image:
self.login_and_go("/system")
b.wait_not_visible("#server-pmlogger-switch")
b.wait_not_visible("#system-information-enable-pcp-link")
b.wait_not_present("#server-pmlogger-switch")
b.wait_not_present("#system-configuration-enable-pcp-link")
return
m.execute("pkcon remove -y pcp")
@ -748,14 +768,23 @@ class TestPcp(packagelib.PackageCase):
# the offer to install it should be visible
self.login_and_go("/system")
b.wait_not_visible("#server-pmlogger-switch")
b.wait_in_text("#system-information-enable-pcp-link", "Enable stored metrics…")
b.click("#system-information-enable-pcp-link")
if m.image in ["rhel-8-1-distropkg"]:
b.wait_not_visible("#server-pmlogger-switch")
b.wait_in_text("#system-information-enable-pcp-link", "Enable stored metrics...")
b.click("#system-information-enable-pcp-link")
else:
b.wait_not_present("#server-pmlogger-switch")
b.wait_in_text("#system-configuration-enable-pcp-link", "Enable stored metrics...")
b.click("#system-configuration-enable-pcp-link")
b.click(".modal-footer button.btn-primary:contains('Install')")
b.wait_not_present(".modal-dialog:contains('Install Software')")
b.wait_visible("#server-pmlogger-switch")
b.wait_not_visible("#system-information-enable-pcp-link")
if m.image in ["rhel-8-1-distropkg"]:
b.wait_not_visible("#system-information-enable-pcp-link")
else:
b.wait_not_present("#system-configuration-enable-pcp-link")
# Turn stored metrics on
b.wait_present("#server-pmlogger-switch input:not(:checked)")

View File

@ -103,22 +103,23 @@ var info = {
"systemd/services": [
"systemd/init.js",
"systemd/services.css",
"systemd/services.less",
],
"systemd/logs": [
"systemd/logs.js",
"systemd/logs.less",
],
"systemd/system": [
"systemd/host.js",
"systemd/host.css",
"systemd/overview": [
"systemd/overview.jsx",
"systemd/overview.less",
],
"systemd/terminal": [
"systemd/terminal.jsx",
"systemd/terminal.css",
"systemd/terminal.less",
],
"systemd/hwinfo": [
"systemd/hwinfo.jsx",
"systemd/hwinfo.css",
"systemd/hwinfo.less",
],
"systemd/graphs": [
"systemd/graphs.js",