Session recording module for Cockpit initial commit
This commit is contained in:
parent
d73a91232e
commit
a20e3c5a81
27 changed files with 5564 additions and 32 deletions
|
|
@ -1,19 +1,19 @@
|
|||
Name: cockpit-starter-kit
|
||||
Name: cockpit-session-recording
|
||||
Version: @VERSION@
|
||||
Release: 1%{?dist}
|
||||
Summary: Cockpit Starter Kit Example Module
|
||||
Summary: Cockpit Session Recording
|
||||
License: LGPLv2+
|
||||
|
||||
Source: cockpit-starter-kit-%{version}.tar.gz
|
||||
Source: cockpit-session-recording-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
%define debug_package %{nil}
|
||||
|
||||
%description
|
||||
Cockpit Starter Kit Example Module
|
||||
Cockpit Session Recording
|
||||
|
||||
%prep
|
||||
%setup -n cockpit-starter-kit
|
||||
%setup -n cockpit-session-recording
|
||||
|
||||
%install
|
||||
%make_install
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<component type="addon">
|
||||
<id>org.cockpit-project.starter-kit</id>
|
||||
<id>org.cockpit-project.session-recording</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<name>Starter Kit</name>
|
||||
<name>Session Recording</name>
|
||||
<summary>
|
||||
Scaffolding for a cockpit module.
|
||||
</summary>
|
||||
|
|
@ -11,5 +11,5 @@
|
|||
</p>
|
||||
</description>
|
||||
<extends>cockpit.desktop</extends>
|
||||
<launchable type="cockpit-manifest">cockpit-starter-kit</launchable>
|
||||
<launchable type="cockpit-manifest">cockpit-session-recording</launchable>
|
||||
</component>
|
||||
19
package.json
19
package.json
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "starter-kit",
|
||||
"name": "session-recording",
|
||||
"version": "0.1.0",
|
||||
"description": "Scaffolding for a cockpit module",
|
||||
"main": "index.js",
|
||||
|
|
@ -34,6 +34,10 @@
|
|||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"htmlparser": "^1.7.7",
|
||||
"jed": "^1.1.1",
|
||||
"jshint": "~2.9.1",
|
||||
"jshint-loader": "~0.8.3",
|
||||
"less": "~3.0.1",
|
||||
"less-loader": "~4.0.6",
|
||||
"po2json": "^0.4.5",
|
||||
"sass-loader": "^7.0.3",
|
||||
"sizzle": "^2.3.3",
|
||||
|
|
@ -46,5 +50,18 @@
|
|||
"node-sass": "^4.9.0",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2"
|
||||
"bootstrap": "3.3.7",
|
||||
"patternfly": "3.35.1",
|
||||
"webpack": "^2.6.1",
|
||||
"jquery": "3.3.1",
|
||||
"moment": "2.22.2",
|
||||
"mustache": "2.3.0",
|
||||
"bootstrap-datetime-picker": "2.4.4",
|
||||
"comment-json": "^1.1.3",
|
||||
"term.js-cockpit": "0.0.10",
|
||||
"fs.extra": "^1.3.2",
|
||||
"fs.realpath": "^1.0.0",
|
||||
"node-sass": "^4.9.0",
|
||||
"raw-loader": "^0.5.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
p {
|
||||
font-weight: bold;
|
||||
}
|
||||
54
src/config.html
Normal file
54
src/config.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is part of Cockpit.
|
||||
|
||||
Copyright (C) 2017 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/>.
|
||||
-->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title translate>Journal</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="../base1/patternfly.css" rel="stylesheet">
|
||||
<link href="table.css" rel="stylesheet">
|
||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
||||
<script src="../*/po.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/cockpit/@localhost/session_recording/index.html">Session Recording</a></li>
|
||||
<li class="active">Configuration</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default" >
|
||||
<div class="panel-heading"><span>Configuration</span></div>
|
||||
<div class="panel-body" id="view"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="config.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
279
src/config.jsx
Normal file
279
src/config.jsx
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2017 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/>.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
let cockpit = require("cockpit");
|
||||
let React = require("react");
|
||||
let json = require('comment-json');
|
||||
|
||||
let Config = class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.setConfig = this.setConfig.bind(this);
|
||||
this.prepareConfig = this.prepareConfig.bind(this);
|
||||
this.fileReadFailed = this.fileReadFailed.bind(this);
|
||||
this.file = null;
|
||||
this.state = {
|
||||
config: null,
|
||||
file_error: null,
|
||||
submitting: "none",
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
||||
const name = e.target.name;
|
||||
const config = this.state.config;
|
||||
config[name] = value;
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
prepareConfig() {
|
||||
this.state.config.latency = parseInt(this.state.config.latency);
|
||||
if (this.state.config.input === true) {
|
||||
// log.input
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
this.setState({submitting:"block"});
|
||||
console.log(event);
|
||||
this.prepareConfig();
|
||||
this.file.replace(this.state.config).done(() => {
|
||||
console.log('updated');
|
||||
this.setState({submitting:"none"});
|
||||
})
|
||||
.fail((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
setConfig(data) {
|
||||
console.log(data);
|
||||
this.setState({config: data});
|
||||
}
|
||||
|
||||
fileReadFailed(reason) {
|
||||
console.log(reason);
|
||||
this.setState({file_error: reason});
|
||||
console.log('failed to read file');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let parseFunc = function(data) {
|
||||
console.log(data);
|
||||
// return data;
|
||||
return json.parse(data, null, true);
|
||||
};
|
||||
|
||||
let stringifyFunc = function(data) {
|
||||
return json.stringify(data, null, true);
|
||||
};
|
||||
// needed for cockpit.file usage
|
||||
let syntax_object = {
|
||||
parse: parseFunc,
|
||||
stringify: stringifyFunc,
|
||||
};
|
||||
|
||||
this.file = cockpit.file("/etc/tlog/tlog-rec-session.conf", {
|
||||
syntax: syntax_object,
|
||||
// binary: boolean,
|
||||
// max_read_size: int,
|
||||
superuser: true,
|
||||
// host: string
|
||||
});
|
||||
|
||||
console.log(this.file);
|
||||
|
||||
let promise = this.file.read();
|
||||
|
||||
promise.done((data) => {
|
||||
if (data === null) {
|
||||
this.fileReadFailed();
|
||||
return;
|
||||
}
|
||||
this.setConfig(data);
|
||||
}).fail((data) => {
|
||||
this.fileReadFailed(data);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.config != null && this.state.file_error === null) {
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<table className="form-table-ct col-sm-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="shell" className="control-label">Shell</label></td>
|
||||
<td>
|
||||
<input type="text" id="shell" name="shell" value={this.state.config.shell}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="notice" className="control-label">Notice</label></td>
|
||||
<td>
|
||||
<input type="text" id="notice" name="notice" value={this.state.config.notice}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="latency" className="control-label">Latency</label></td>
|
||||
<td>
|
||||
<input type="text" id="latency" name="latency" value={this.state.config.latency}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="latency" className="control-label">Payload Size, bytes</label></td>
|
||||
<td>
|
||||
<input type="text" id="payload" name="payload" value={this.state.config.payload}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="input" className="control-label">Log User's Input</label></td>
|
||||
<td>
|
||||
<input type="checkbox" id="input" name="input" defaultChecked={this.state.config.log.input}
|
||||
onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="output" className="control-label">Log User's Output</label></td>
|
||||
<td>
|
||||
<input type="checkbox" id="output" name="output" defaultChecked={this.state.config.log.output}
|
||||
onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="window" className="control-label">Log Window Resize</label></td>
|
||||
<td>
|
||||
<input type="checkbox" id="window" name="window" defaultChecked={this.state.config.log.window}
|
||||
onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="rate" className="control-label">Limit Rate, bytes/sec</label></td>
|
||||
<td>
|
||||
<input type="text" id="rate" name="rate" value={this.state.config.limit.rate}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="burst" className="control-label">Burst, bytes</label></td>
|
||||
<td>
|
||||
<input type="text" id="burst" name="burst" value={this.state.config.limit.burst}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="action" className="control-label">Logging Limit Action</label></td>
|
||||
<td>
|
||||
<select name="action" id="action" onChange={this.handleInputChange} value={this.state.config.limit.action}>
|
||||
<option value="" />
|
||||
<option value="pass">Pass</option>
|
||||
<option value="delay">Delay</option>
|
||||
<option value="drop">Drop</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="path" className="control-label">File Path</label></td>
|
||||
<td>
|
||||
<input type="text" id="path" name="path" defaultChecked={this.state.config.file.path}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="facility" className="control-label">Syslog Facility</label></td>
|
||||
<td>
|
||||
<input type="text" id="facility" name="facility" value={this.state.config.syslog.facility}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="syslog_priority" className="control-label">Syslog Priority</label></td>
|
||||
<td>
|
||||
<input type="text" id="syslog_priority" name="syslog_priority" value={this.state.config.syslog.priority}
|
||||
className="form-control" onChange={this.handleInputChange} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="path" className="control-label">Journal Priority</label></td>
|
||||
<td>
|
||||
<select name="journal_priority" id="journal_priority" onChange={this.handleInputChange} value={this.state.config.journal.priority}>
|
||||
<option value="" />
|
||||
<option value="info">Info</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="path" className="control-label">Journal Augment</label></td>
|
||||
<td>
|
||||
<input type="checkbox" id="augment" name="augment" defaultChecked={this.state.config.journal.augment}
|
||||
onChange={this.handleInputChange} />
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top"><label htmlFor="path" className="control-label">Writer</label></td>
|
||||
<td>
|
||||
<select name="writer" id="writer" onChange={this.handleInputChange} value={this.state.config.writer}>
|
||||
<option value="" />
|
||||
<option value="journal">Journal</option>
|
||||
<option value="syslog">Syslog</option>
|
||||
<option value="file">File</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="top" />
|
||||
<div className="spinner spinner-sm" style={{display: this.state.submitting}} />
|
||||
<td>
|
||||
<button className="btn btn-default" type="submit">Save</button>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="alert alert-danger">
|
||||
<span className="pficon pficon-error-circle-o" />
|
||||
<p><strong>There is no configuration file of tlog present in your system.</strong></p>
|
||||
<p>Please, check the /etc/tlog/tlog-rec-session.conf or if tlog is installed.</p>
|
||||
<p><strong>{this.state.file_error}</strong></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.render(<Config />, document.getElementById('view'));
|
||||
}());
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is part of Cockpit.
|
||||
|
||||
Copyright (C) 2017 Red Hat, Inc.
|
||||
|
||||
Cockpit is free software; you can redistribute it and/or modify it
|
||||
|
|
@ -13,24 +15,23 @@ 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 this package; If not, see <http://www.gnu.org/licenses/>.
|
||||
along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<html lang="en">
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title translatable="yes">Cockpit Starter Kit</title>
|
||||
<meta charset="utf-8">
|
||||
<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="index.css">
|
||||
|
||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
||||
<script type="text/javascript" src="../*/po.js"></script>
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
<title translate>Journal</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="../base1/patternfly.css" rel="stylesheet">
|
||||
<link href="recordings.css" rel="stylesheet">
|
||||
<script type="text/javascript" src="../base1/jquery.js"></script>
|
||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
||||
<script src="../*/po.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="view"/>
|
||||
<script type="text/javascript" src="recordings.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"version": "0.1",
|
||||
"version": "163.x",
|
||||
"name": "session_recording",
|
||||
|
||||
"requires": {
|
||||
"cockpit": "137"
|
||||
"cockpit": "122"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"menu": {
|
||||
"index": {
|
||||
"label": "Starter Kit"
|
||||
"label": "Session Recording",
|
||||
"order": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/manifest.json.in
Normal file
15
src/manifest.json.in
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "@VERSION@",
|
||||
"name": "session_recording",
|
||||
|
||||
"requires": {
|
||||
"cockpit": "122"
|
||||
},
|
||||
|
||||
"menu": {
|
||||
"index": {
|
||||
"label": "Session Recording",
|
||||
"order": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
360
src/pkg/lib/cockpit-components-listing.jsx
Normal file
360
src/pkg/lib/cockpit-components-listing.jsx
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2016 Red Hat, Inc.
|
||||
*
|
||||
* Cockpit is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Cockpit is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var React = require('react');
|
||||
|
||||
require('./listing.less');
|
||||
|
||||
/* entry for an alert in the listing, can be expanded (with details) or standard
|
||||
* rowId optional: an identifier for the row which will be set as "data-row-id" attribute on the <tr>
|
||||
* columns list of columns to show in the header
|
||||
* columns to show, can be a string, react component or object with { name: 'name', 'header': false }
|
||||
* 'header' (or if simple string) defaults to false
|
||||
* in case 'header' is true, <th> is used for the entries, otherwise <td>
|
||||
* tabRenderers optional: list of tab renderers for inline expansion, array of objects with
|
||||
* - name tab name (has to be unique in the entry, used as react key)
|
||||
* - renderer react component
|
||||
* - data render data passed to the tab renderer
|
||||
* - presence 'always', 'onlyActive', 'loadOnDemand', default: 'loadOnDemand'
|
||||
* - 'always' once a row is expanded, this tab is always rendered, but invisible if not active
|
||||
* - 'onlyActive' the tab is only rendered when active
|
||||
* - 'loadOnDemand' the tab is first rendered when it becomes active, then follows 'always' behavior
|
||||
* if tabRenderers isn't set, item can't be expanded inline
|
||||
* navigateToItem optional: callback triggered when a row is clicked, pattern suggests navigation
|
||||
* to view expanded item details, if not set, navigation isn't available
|
||||
* listingDetail optional: text rendered next to action buttons, similar style to the tab headers
|
||||
* listingActions optional: buttons that are presented as actions for the expanded item
|
||||
* selectChanged optional: callback will be used when the "selected" state changes
|
||||
* selected optional: true if the item is selected, can't be true if row has navigation or expansion
|
||||
* initiallyExpanded optional: the entry will be initially rendered as expanded, but then behaves normally
|
||||
* expandChanged optional: callback will be used if the row is either expanded or collapsed passing single `isExpanded` boolean argument
|
||||
*/
|
||||
var ListingRow = React.createClass({
|
||||
propTypes: {
|
||||
rowId: React.PropTypes.string,
|
||||
columns: React.PropTypes.array.isRequired,
|
||||
tabRenderers: React.PropTypes.array,
|
||||
navigateToItem: React.PropTypes.func,
|
||||
listingDetail: React.PropTypes.node,
|
||||
listingActions: React.PropTypes.arrayOf(React.PropTypes.node),
|
||||
selectChanged: React.PropTypes.func,
|
||||
selected: React.PropTypes.bool,
|
||||
initiallyExpanded: React.PropTypes.bool,
|
||||
expandChanged: React.PropTypes.func,
|
||||
initiallyActiveTab: React.PropTypes.bool,
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
tabRenderers: [],
|
||||
navigateToItem: null,
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
expanded: this.props.initiallyExpanded, // show expanded view if true, otherwise one line compact
|
||||
activeTab: this.props.initiallyActiveTab ? this.props.initiallyActiveTab : 0, // currently active tab in expanded mode, defaults to first tab
|
||||
loadedTabs: {}, // which tabs were already loaded - this is important for 'loadOnDemand' setting
|
||||
// contains tab indices
|
||||
selected: this.props.selected, // whether the current row is selected
|
||||
};
|
||||
},
|
||||
handleNavigateClick: function(e) {
|
||||
// only consider primary mouse button
|
||||
if (!e || e.button !== 0)
|
||||
return;
|
||||
this.props.navigateToItem();
|
||||
},
|
||||
handleExpandClick: function(e) {
|
||||
// only consider primary mouse button
|
||||
if (!e || e.button !== 0)
|
||||
return;
|
||||
|
||||
var willBeExpanded = !this.state.expanded && this.props.tabRenderers.length > 0;
|
||||
this.setState({ expanded: willBeExpanded });
|
||||
|
||||
var loadedTabs = {};
|
||||
// unload all tabs if not expanded
|
||||
if (willBeExpanded) {
|
||||
// see if we should preload some tabs
|
||||
var tabIdx;
|
||||
var tabPresence;
|
||||
for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) {
|
||||
if ('presence' in this.props.tabRenderers[tabIdx])
|
||||
tabPresence = this.props.tabRenderers[tabIdx].presence;
|
||||
else
|
||||
tabPresence = 'default';
|
||||
// the active tab is covered by separate logic
|
||||
if (tabPresence == 'always')
|
||||
loadedTabs[tabIdx] = true;
|
||||
}
|
||||
// ensure the active tab is loaded
|
||||
loadedTabs[this.state.activeTab] = true;
|
||||
}
|
||||
|
||||
this.setState({ loadedTabs: loadedTabs });
|
||||
|
||||
this.props.expandChanged && this.props.expandChanged(willBeExpanded);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
handleSelectClick: function(e) {
|
||||
// only consider primary mouse button
|
||||
if (!e || e.button !== 0)
|
||||
return;
|
||||
|
||||
var selected = !this.state.selected;
|
||||
this.setState({ selected: selected });
|
||||
|
||||
if (this.props.selectChanged)
|
||||
this.props.selectChanged(selected);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
handleTabClick: function(tabIdx, e) {
|
||||
// only consider primary mouse button
|
||||
if (!e || e.button !== 0)
|
||||
return;
|
||||
var prevTab = this.state.activeTab;
|
||||
var prevTabPresence = 'default';
|
||||
var loadedTabs = this.state.loadedTabs;
|
||||
if (prevTab !== tabIdx) {
|
||||
// see if we need to unload the previous tab
|
||||
if ('presence' in this.props.tabRenderers[prevTab])
|
||||
prevTabPresence = this.props.tabRenderers[prevTab].presence;
|
||||
|
||||
if (prevTabPresence == 'onlyActive')
|
||||
delete loadedTabs[prevTab];
|
||||
|
||||
// ensure the new tab is loaded and update state
|
||||
loadedTabs[tabIdx] = true;
|
||||
this.setState({ loadedTabs: loadedTabs, activeTab: tabIdx });
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
// only enable navigation if a function is provided and the row isn't expanded (prevent accidental navigation)
|
||||
var allowNavigate = !!this.props.navigateToItem && !this.state.expanded;
|
||||
|
||||
var headerEntries = this.props.columns.map(function(itm) {
|
||||
if (typeof itm === 'string' || typeof itm === 'number' || itm === null || itm === undefined || itm instanceof String || React.isValidElement(itm))
|
||||
return (<td>{itm}</td>);
|
||||
else if ('header' in itm && itm.header)
|
||||
return (<th>{itm.name}</th>);
|
||||
else if ('tight' in itm && itm.tight)
|
||||
return (<td className="listing-ct-actions">{itm.name || itm.element}</td>);
|
||||
else
|
||||
return (<td>{itm.name}</td>);
|
||||
});
|
||||
|
||||
var allowExpand = (this.props.tabRenderers.length > 0);
|
||||
var expandToggle;
|
||||
if (allowExpand) {
|
||||
expandToggle = <td className="listing-ct-toggle" onClick={ allowNavigate ? this.handleExpandClick : undefined }>
|
||||
<i className="fa fa-fw" />
|
||||
</td>;
|
||||
} else {
|
||||
expandToggle = <td className="listing-ct-toggle" />;
|
||||
}
|
||||
|
||||
var listingItemClasses = ["listing-ct-item"];
|
||||
if (!allowNavigate)
|
||||
listingItemClasses.push("listing-ct-nonavigate");
|
||||
if (!allowExpand)
|
||||
listingItemClasses.push("listing-ct-noexpand");
|
||||
|
||||
var allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined);
|
||||
var clickHandler;
|
||||
if (allowSelect) {
|
||||
clickHandler = this.handleSelectClick;
|
||||
if (this.state.selected)
|
||||
listingItemClasses.push("listing-ct-selected");
|
||||
} else {
|
||||
if (allowNavigate)
|
||||
clickHandler = this.handleNavigateClick;
|
||||
else
|
||||
clickHandler = this.handleExpandClick;
|
||||
}
|
||||
|
||||
var listingItem = (
|
||||
<tr data-row-id={ this.props.rowId }
|
||||
className={ listingItemClasses.join(' ') }
|
||||
onClick={clickHandler}>
|
||||
{expandToggle}
|
||||
{headerEntries}
|
||||
</tr>
|
||||
);
|
||||
|
||||
if (this.state.expanded) {
|
||||
var links = this.props.tabRenderers.map(function(itm, idx) {
|
||||
return (
|
||||
<li key={idx} className={ (idx === self.state.activeTab) ? "active" : ""} >
|
||||
<a href="#" tabIndex="0" onClick={ self.handleTabClick.bind(self, idx) }>{itm.name}</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
var tabs = [];
|
||||
var tabIdx;
|
||||
var Renderer;
|
||||
var rendererData;
|
||||
var row;
|
||||
for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) {
|
||||
Renderer = this.props.tabRenderers[tabIdx].renderer;
|
||||
rendererData = this.props.tabRenderers[tabIdx].data;
|
||||
if (tabIdx !== this.state.activeTab && !(tabIdx in this.state.loadedTabs))
|
||||
continue;
|
||||
row = <Renderer key={ this.props.tabRenderers[tabIdx].name } hidden={ (tabIdx !== this.state.activeTab) } {...rendererData} />;
|
||||
if (tabIdx === this.state.activeTab)
|
||||
tabs.push(<div className="listing-ct-body" key={tabIdx}>{row}</div>);
|
||||
else
|
||||
tabs.push(<div className="listing-ct-body" key={tabIdx} hidden>{row}</div>);
|
||||
}
|
||||
|
||||
var listingDetail;
|
||||
if ('listingDetail' in this.props) {
|
||||
listingDetail = (
|
||||
<span className="listing-ct-caption">
|
||||
{this.props.listingDetail}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody className="open">
|
||||
{listingItem}
|
||||
<tr className="listing-ct-panel">
|
||||
<td colSpan={ headerEntries.length + (expandToggle ? 1 : 0) }>
|
||||
<div className="listing-ct-head">
|
||||
<div className="listing-ct-actions">
|
||||
{listingDetail}
|
||||
{this.props.listingActions}
|
||||
</div>
|
||||
<ul className="nav nav-tabs nav-tabs-pf">
|
||||
{links}
|
||||
</ul>
|
||||
</div>
|
||||
{tabs}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<tbody>
|
||||
{listingItem}
|
||||
<tr className="listing-ct-panel" />
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* Implements a PatternFly 'List View' pattern
|
||||
* https://www.patternfly.org/list-view/
|
||||
* Properties:
|
||||
* - title
|
||||
* - fullWidth optional: set width to 100% of parent, defaults to true
|
||||
* - emptyCaption header caption to show if list is empty, defaults to "No entries"
|
||||
* - columnTitles: array of column titles, as strings
|
||||
* - columnTitleClick: optional callback for clicking on column title (for sorting)
|
||||
* receives the column index as argument
|
||||
* - actions: additional listing-wide actions (displayed next to the list's title)
|
||||
*/
|
||||
var Listing = React.createClass({
|
||||
propTypes: {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
fullWidth: React.PropTypes.bool,
|
||||
emptyCaption: React.PropTypes.string.isRequired,
|
||||
columnTitles: React.PropTypes.arrayOf(React.PropTypes.string),
|
||||
columnTitleClick: React.PropTypes.func,
|
||||
actions: React.PropTypes.arrayOf(React.PropTypes.node)
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
fullWidth: true,
|
||||
columnTitles: [],
|
||||
actions: []
|
||||
};
|
||||
},
|
||||
render: function() {
|
||||
var self = this;
|
||||
var bodyClasses = ["listing", "listing-ct"];
|
||||
if (this.props.fullWidth)
|
||||
bodyClasses.push("listing-ct-wide");
|
||||
var headerClasses;
|
||||
var headerRow;
|
||||
var selectableRows;
|
||||
if (!this.props.children || this.props.children.length === 0) {
|
||||
headerClasses = "listing-ct-empty";
|
||||
headerRow = <tr><td>{this.props.emptyCaption}</td></tr>;
|
||||
} else if (this.props.columnTitles.length) {
|
||||
// check if any of the children are selectable
|
||||
selectableRows = false;
|
||||
this.props.children.forEach(function(r) {
|
||||
if (r.props.selected !== undefined)
|
||||
selectableRows = true;
|
||||
});
|
||||
|
||||
if (selectableRows) {
|
||||
// now make sure that if one is set, it's available on all items
|
||||
this.props.children.forEach(function(r) {
|
||||
if (r.props.selected === undefined)
|
||||
r.props.selected = false;
|
||||
});
|
||||
}
|
||||
|
||||
headerRow = (
|
||||
<tr>
|
||||
<th className="listing-ct-toggle" />
|
||||
{ this.props.columnTitles.map(function (title, index) {
|
||||
var clickHandler = null;
|
||||
if (self.props.columnTitleClick)
|
||||
clickHandler = function() { self.props.columnTitleClick(index) };
|
||||
return <th onClick={clickHandler}>{title}</th>;
|
||||
}) }
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
headerRow = <tr />
|
||||
}
|
||||
var caption;
|
||||
if (this.props.title || (this.props.actions && this.props.actions.length > 0))
|
||||
caption = <caption className="cockpit-caption">{this.props.title}{this.props.actions}</caption>;
|
||||
|
||||
return (
|
||||
<table className={ bodyClasses.join(" ") }>
|
||||
{caption}
|
||||
<thead className={headerClasses}>
|
||||
{headerRow}
|
||||
</thead>
|
||||
{this.props.children}
|
||||
</table>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
ListingRow: ListingRow,
|
||||
Listing: Listing,
|
||||
};
|
||||
59
src/pkg/lib/console.css
Normal file
59
src/pkg/lib/console.css
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@import "term.css";
|
||||
|
||||
/* Our terminal or logs */
|
||||
.console-ct {
|
||||
font-family: Menlo, Monaco, Consolas, monospace;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
@media (min-width: 568px) {
|
||||
.console-ct {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.console-ct > pre {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
width: 48em;
|
||||
height: 310px;
|
||||
overflow-y: scroll;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.console-ct > .terminal {
|
||||
color: #F0F0F0;
|
||||
text-align: left;
|
||||
outline: medium none;
|
||||
background-color: black;
|
||||
border: 1px solid black;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.terminal .terminal-cursor {
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.terminal:focus .terminal-cursor {
|
||||
border: none;
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
from {
|
||||
color: #000;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
50% {
|
||||
color: #f0f0f0;
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
134
src/pkg/lib/journal.css
Normal file
134
src/pkg/lib/journal.css
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2015 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/>.
|
||||
*/
|
||||
|
||||
.cockpit-log-panel {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.cockpit-log-panel .panel-heading {
|
||||
background-color: #333;
|
||||
border-color: #333;
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.cockpit-log-panel .panel-body {
|
||||
padding: 0;
|
||||
border-bottom: 1px #ddd solid;
|
||||
}
|
||||
|
||||
.cockpit-log-panel .panel-body .panel-heading {
|
||||
border-left: 1px #ddd solid;
|
||||
border-right: 1px #ddd solid;
|
||||
border-top: 0;
|
||||
border-bottom: 1px #ddd solid;
|
||||
background-color: #f5f5f5;
|
||||
font-weight: bold;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
width: auto;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cockpit-log-panel > .panel-heading {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cockpit-log-panel .cockpit-logline {
|
||||
border-left: 1px #ddd solid;
|
||||
border-right: 1px #ddd solid;
|
||||
background-color: #f5f5f5;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cockpit-logline {
|
||||
font-family: monospace;
|
||||
min-width: 310px;
|
||||
border-bottom: 1px solid #DDD;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.cockpit-logline > .row > div:first-child {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.cockpit-log-panel .cockpit-logline:hover {
|
||||
background-color: #d4edfa;
|
||||
}
|
||||
|
||||
.cockpit-log-panel > .cockpit-logline:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cockpit-logmsg-reboot {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cockpit-log-warning {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cockpit-log-warning > i {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.cockpit-log-time {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cockpit-log-service {
|
||||
width: 200px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cockpit-log-service-container {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cockpit-log-service-reduced {
|
||||
width: -moz-calc(100% - 70px);
|
||||
width: -webkit-calc(100% - 70px);
|
||||
width: calc(100% - 70px);
|
||||
}
|
||||
|
||||
.cockpit-log-message {
|
||||
width: -moz-calc(100% - 300px);
|
||||
width: -webkit-calc(100% - 300px);
|
||||
width: calc(100% - 300px);
|
||||
}
|
||||
|
||||
.cockpit-log-message, .cockpit-log-service, .cockpit-log-service-reduced {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
584
src/pkg/lib/journal.js
Normal file
584
src/pkg/lib/journal.js
Normal file
|
|
@ -0,0 +1,584 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2015 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/>.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
var cockpit = require("cockpit");
|
||||
var Mustache = require("mustache");
|
||||
var day_header_template = require('raw-loader!journal_day_header.mustache');
|
||||
var line_template = require('raw-loader!journal_line.mustache');
|
||||
var reboot_template = require('raw-loader!journal_reboot.mustache');
|
||||
|
||||
var _ = cockpit.gettext;
|
||||
var C_ = cockpit.gettext;
|
||||
|
||||
var journal = { };
|
||||
|
||||
/**
|
||||
* journalctl([match, ...], [options])
|
||||
* @match: any number of journal match strings
|
||||
* @options: an object containing further options
|
||||
*
|
||||
* Load and (by default) stream journal entries as
|
||||
* json objects. This function returns a jQuery deferred
|
||||
* object which delivers the various journal entries.
|
||||
*
|
||||
* The various @match strings are journalctl matches.
|
||||
* Zero, one or more can be specified. They must be in
|
||||
* string format, or arrays of strings.
|
||||
*
|
||||
* The optional @options object can contain the following:
|
||||
* * "host": the host to load journal from
|
||||
* * "count": number of entries to load and/or pre-stream.
|
||||
* Default is 10
|
||||
* * "follow": if set to false just load entries and don't
|
||||
* stream further journal data. Default is true.
|
||||
* * "directory": optional directory to load journal files
|
||||
* * "boot": when set only list entries from this specific
|
||||
* boot id, or if null then the current boot.
|
||||
* * "since": if specified list entries since the date/time
|
||||
* * "until": if specified list entries until the date/time
|
||||
* * "cursor": a cursor to start listing entries from
|
||||
* * "after": a cursor to start listing entries after
|
||||
*
|
||||
* Returns a jQuery deferred promise. You can call these
|
||||
* functions on the deferred to handle the responses. Note that
|
||||
* there are additional non-jQuery methods.
|
||||
*
|
||||
* .done(function(entries) { }): Called when done, @entries is
|
||||
* an array of all journal entries loaded. If .stream()
|
||||
* has been invoked then @entries will be empty.
|
||||
* .fail(funciton(ex) { }): called if the operation fails
|
||||
* .stream(function(entries) { }): called when we receive entries
|
||||
* entries. Called once per batch of journal @entries,
|
||||
* whether following or not.
|
||||
* .stop(): stop following or retrieving entries.
|
||||
*/
|
||||
|
||||
journal.journalctl = function journalctl(/* ... */) {
|
||||
var matches = [];
|
||||
var i, arg, options = { follow: true };
|
||||
for (i = 0; i < arguments.length; i++) {
|
||||
arg = arguments[i];
|
||||
if (typeof arg == "string") {
|
||||
matches.push(arg);
|
||||
} else if (typeof arg == "object") {
|
||||
if (arg instanceof Array) {
|
||||
matches.push.apply(matches, arg);
|
||||
} else {
|
||||
cockpit.extend(options, arg);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
console.warn("journal.journalctl called with invalid argument:", arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.count === undefined) {
|
||||
if (options.follow)
|
||||
options.count = 10;
|
||||
else
|
||||
options.count = null;
|
||||
}
|
||||
|
||||
var cmd = [ "journalctl", "-q", "--output=json" ];
|
||||
if (!options.count)
|
||||
cmd.push("--no-tail");
|
||||
else
|
||||
cmd.push("--lines=" + options.count);
|
||||
if (options.directory)
|
||||
cmd.push("--directory=" + options.directory);
|
||||
if (options.boot)
|
||||
cmd.push("--boot=" + options.boot);
|
||||
else if (options.boot !== undefined)
|
||||
cmd.push("--boot");
|
||||
if (options.since)
|
||||
cmd.push("--since=" + options.since);
|
||||
if (options.until)
|
||||
cmd.push("--until=" + options.until);
|
||||
if (options.cursor)
|
||||
cmd.push("--cursor=" + options.cursor);
|
||||
if (options.after)
|
||||
cmd.push("--after=" + options.after);
|
||||
|
||||
/* journalctl doesn't allow reverse and follow together */
|
||||
if (options.reverse)
|
||||
cmd.push("--reverse");
|
||||
else if (options.follow)
|
||||
cmd.push("--follow");
|
||||
|
||||
cmd.push("--");
|
||||
cmd.push.apply(cmd, matches);
|
||||
|
||||
var dfd = new cockpit.defer();
|
||||
var promise;
|
||||
var buffer = "";
|
||||
var entries = [];
|
||||
var streamers = [];
|
||||
var interval = null;
|
||||
|
||||
function fire_streamers() {
|
||||
var ents, i;
|
||||
if (streamers.length && entries.length > 0) {
|
||||
ents = entries;
|
||||
entries = [];
|
||||
for (i = 0; i < streamers.length; i++)
|
||||
streamers[i].apply(promise, [ents]);
|
||||
} else {
|
||||
window.clearInterval(interval);
|
||||
interval = null;
|
||||
}
|
||||
}
|
||||
|
||||
var proc = cockpit.spawn(cmd, { host: options.host, batch: 8192, latency: 300, superuser: "try" }).
|
||||
stream(function(data) {
|
||||
|
||||
if (buffer)
|
||||
data = buffer + data;
|
||||
buffer = "";
|
||||
|
||||
var lines = data.split("\n");
|
||||
var last = lines.length - 1;
|
||||
lines.forEach(function(line, i) {
|
||||
if (i == last) {
|
||||
buffer = line;
|
||||
} else if (line && line.indexOf("-- ") !== 0) {
|
||||
try {
|
||||
entries.push(JSON.parse(line));
|
||||
} catch (e) {
|
||||
console.warn(e, line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (streamers.length && interval === null)
|
||||
interval = window.setInterval(fire_streamers, 300);
|
||||
}).
|
||||
done(function() {
|
||||
fire_streamers();
|
||||
dfd.resolve(entries);
|
||||
}).
|
||||
fail(function(ex) {
|
||||
/* The journalctl command fails when no entries are matched
|
||||
* so we just ignore this status code */
|
||||
if (ex.problem == "cancelled" ||
|
||||
ex.exit_status === 1) {
|
||||
fire_streamers();
|
||||
dfd.resolve(entries);
|
||||
} else {
|
||||
dfd.reject(ex);
|
||||
}
|
||||
}).
|
||||
always(function() {
|
||||
window.clearInterval(interval);
|
||||
});
|
||||
|
||||
promise = dfd.promise();
|
||||
promise.stream = function stream(callback) {
|
||||
streamers.push(callback);
|
||||
return this;
|
||||
};
|
||||
promise.stop = function stop() {
|
||||
proc.close("cancelled");
|
||||
};
|
||||
return promise;
|
||||
};
|
||||
|
||||
journal.printable = function printable(value) {
|
||||
if (value === undefined || value === null)
|
||||
return _("[no data]");
|
||||
else if (typeof(value) == "string")
|
||||
return value;
|
||||
else if (value.length !== undefined)
|
||||
return cockpit.format(_("[$0 bytes of binary data]"), value.length);
|
||||
else
|
||||
return _("[binary data]");
|
||||
};
|
||||
|
||||
function output_funcs_for_box(box) {
|
||||
/* Dereference any jQuery object here */
|
||||
if (box.jquery)
|
||||
box = box[0];
|
||||
|
||||
Mustache.parse(day_header_template);
|
||||
Mustache.parse(line_template);
|
||||
Mustache.parse(reboot_template);
|
||||
|
||||
function render_line(ident, prio, message, count, time, entry) {
|
||||
var parts = {
|
||||
'cursor': entry["__CURSOR"],
|
||||
'time': time,
|
||||
'message': message,
|
||||
'service': ident
|
||||
};
|
||||
if (count > 1)
|
||||
parts['count'] = count;
|
||||
if (ident === 'abrt-notification') {
|
||||
parts['problem'] = true;
|
||||
parts['service'] = entry['PROBLEM_BINARY'];
|
||||
}
|
||||
else if (prio < 4)
|
||||
parts['warning'] = true;
|
||||
return Mustache.render(line_template, parts);
|
||||
}
|
||||
|
||||
var reboot = _("Reboot");
|
||||
var reboot_line = Mustache.render(reboot_template, {'message': reboot} );
|
||||
|
||||
function render_reboot_separator() {
|
||||
return reboot_line;
|
||||
}
|
||||
|
||||
function render_day_header(day) {
|
||||
return Mustache.render(day_header_template, {'day': day} );
|
||||
}
|
||||
|
||||
function parse_html(string) {
|
||||
var div = document.createElement("div");
|
||||
div.innerHTML = string.trim();
|
||||
return div.children[0];
|
||||
}
|
||||
|
||||
return {
|
||||
render_line: render_line,
|
||||
render_day_header: render_day_header,
|
||||
render_reboot_separator: render_reboot_separator,
|
||||
|
||||
append: function(elt) {
|
||||
if (typeof (elt) == "string")
|
||||
elt = parse_html(elt);
|
||||
box.appendChild(elt);
|
||||
},
|
||||
prepend: function(elt) {
|
||||
if (typeof (elt) == "string")
|
||||
elt = parse_html(elt);
|
||||
if (box.firstChild)
|
||||
box.insertBefore(elt, box.firstChild);
|
||||
else
|
||||
box.appendChild(elt);
|
||||
},
|
||||
remove_last: function() {
|
||||
if (box.lastChild)
|
||||
box.removeChild(box.lastChild);
|
||||
},
|
||||
remove_first: function() {
|
||||
if (box.firstChild)
|
||||
box.removeChild(box.firstChild);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
var month_names = [
|
||||
C_("month-name", 'January'),
|
||||
C_("month-name", 'February'),
|
||||
C_("month-name", 'March'),
|
||||
C_("month-name", 'April'),
|
||||
C_("month-name", 'May'),
|
||||
C_("month-name", 'June'),
|
||||
C_("month-name", 'July'),
|
||||
C_("month-name", 'August'),
|
||||
C_("month-name", 'September'),
|
||||
C_("month-name", 'October'),
|
||||
C_("month-name", 'November'),
|
||||
C_("month-name", 'December')
|
||||
];
|
||||
|
||||
/* Render the journal entries by passing suitable HTML strings back to
|
||||
the caller via the 'output_funcs'.
|
||||
|
||||
Rendering is context aware. It will insert 'reboot' markers, for
|
||||
example, and collapse repeated lines. You can extend the output at
|
||||
the bottom and also at the top.
|
||||
|
||||
A new renderer is created by calling 'journal.renderer' like
|
||||
so:
|
||||
|
||||
var renderer = journal.renderer(funcs);
|
||||
|
||||
You can feed new entries into the renderer by calling various
|
||||
methods on the returned object:
|
||||
|
||||
- renderer.append(journal_entry)
|
||||
- renderer.append_flush()
|
||||
- renderer.prepend(journal_entry)
|
||||
- renderer.prepend_flush()
|
||||
|
||||
A 'journal_entry' is one element of the result array returned by a
|
||||
call to 'Query' with the 'cockpit.journal_fields' as the fields to
|
||||
return.
|
||||
|
||||
Calling 'append' will append the given entry to the end of the
|
||||
output, naturally, and 'prepend' will prepend it to the start.
|
||||
|
||||
The output might lag behind what has been input via 'append' and
|
||||
'prepend', and you need to call 'append_flush' and 'prepend_flush'
|
||||
respectively to ensure that the output is up-to-date. Flushing a
|
||||
renderer does not introduce discontinuities into the output. You
|
||||
can continue to feed entries into the renderer after flushing and
|
||||
repeated lines will be correctly collapsed across the flush, for
|
||||
example.
|
||||
|
||||
The renderer will call methods of the 'output_funcs' object to
|
||||
produce the desired output:
|
||||
|
||||
- output_funcs.append(rendered)
|
||||
- output_funcs.remove_last()
|
||||
- output_funcs.prepend(rendered)
|
||||
- output_funcs.remove_first()
|
||||
|
||||
The 'rendered' argument is the return value of one of the rendering
|
||||
functions described below. The 'append' and 'prepend' methods
|
||||
should add this element to the output, naturally, and 'remove_last'
|
||||
and 'remove_first' should remove the indicated element.
|
||||
|
||||
If you never call 'prepend' on the renderer, 'output_func.prepend'
|
||||
isn't called either. If you never call 'renderer.prepend' after
|
||||
'renderer.prepend_flush', then 'output_func.remove_first' will
|
||||
never be called. The same guarantees exist for the 'append' family
|
||||
of functions.
|
||||
|
||||
The actual rendering is also done by calling methods on
|
||||
'output_funcs':
|
||||
|
||||
- output_funcs.render_line(ident, prio, message, count, time, cursor)
|
||||
- output_funcs.render_day_header(day)
|
||||
- output_funcs.render_reboot_separator()
|
||||
|
||||
*/
|
||||
|
||||
journal.renderer = function renderer(funcs_or_box) {
|
||||
var output_funcs;
|
||||
if (funcs_or_box.render_line)
|
||||
output_funcs = funcs_or_box;
|
||||
else
|
||||
output_funcs = output_funcs_for_box(funcs_or_box);
|
||||
|
||||
function copy_object(o) {
|
||||
var c = { }; for(var p in o) c[p] = o[p]; return c;
|
||||
}
|
||||
|
||||
// A 'entry' object describes a journal entry in formatted form.
|
||||
// It has fields 'bootid', 'ident', 'prio', 'message', 'time',
|
||||
// 'day', all of which are strings.
|
||||
|
||||
function format_entry(journal_entry) {
|
||||
function pad(n) {
|
||||
var str = n.toFixed();
|
||||
if(str.length == 1)
|
||||
str = '0' + str;
|
||||
return str;
|
||||
}
|
||||
|
||||
var d = new Date(journal_entry["__REALTIME_TIMESTAMP"] / 1000);
|
||||
return {
|
||||
cursor: journal_entry["__CURSOR"],
|
||||
full: journal_entry,
|
||||
day: month_names[d.getMonth()] + ' ' + d.getDate().toFixed() + ', ' + d.getFullYear().toFixed(),
|
||||
time: pad(d.getHours()) + ':' + pad(d.getMinutes()),
|
||||
bootid: journal_entry["_BOOT_ID"],
|
||||
ident: journal_entry["SYSLOG_IDENTIFIER"] || journal_entry["_COMM"],
|
||||
prio: journal_entry["PRIORITY"],
|
||||
message: journal.printable(journal_entry["MESSAGE"])
|
||||
};
|
||||
}
|
||||
|
||||
function entry_is_equal(a, b) {
|
||||
return (a && b &&
|
||||
a.day == b.day &&
|
||||
a.bootid == b.bootid &&
|
||||
a.ident == b.ident &&
|
||||
a.prio == b.prio &&
|
||||
a.message == b.message);
|
||||
}
|
||||
|
||||
// A state object describes a line that should be eventually
|
||||
// output. It has an 'entry' field as per description above, and
|
||||
// also 'count', 'last_time', and 'first_time', which record
|
||||
// repeated entries. Additionally:
|
||||
//
|
||||
// line_present: When true, the line has been output already with
|
||||
// some preliminary data. It needs to be removed before
|
||||
// outputting more recent data.
|
||||
//
|
||||
// header_present: The day header has been output preliminarily
|
||||
// before the actual log lines. It needs to be removed before
|
||||
// prepending more lines. If both line_present and
|
||||
// header_present are true, then the header comes first in the
|
||||
// output, followed by the line.
|
||||
|
||||
function render_state_line(state) {
|
||||
return output_funcs.render_line(state.entry.ident,
|
||||
state.entry.prio,
|
||||
state.entry.message,
|
||||
state.count,
|
||||
state.last_time,
|
||||
state.entry.full);
|
||||
}
|
||||
|
||||
// We keep the state of the first and last journal lines,
|
||||
// respectively, in order to collapse repeated lines, and to
|
||||
// insert reboot markers and day headers.
|
||||
//
|
||||
// Normally, there are two state objects, but if only a single
|
||||
// line has been output so far, top_state and bottom_state point
|
||||
// to the same object.
|
||||
|
||||
var top_state, bottom_state;
|
||||
|
||||
top_state = bottom_state = { };
|
||||
|
||||
function start_new_line() {
|
||||
// If we now have two lines, split the state
|
||||
if (top_state === bottom_state && top_state.entry) {
|
||||
top_state = copy_object(bottom_state);
|
||||
}
|
||||
}
|
||||
|
||||
function top_output() {
|
||||
if (top_state.header_present) {
|
||||
output_funcs.remove_first();
|
||||
top_state.header_present = false;
|
||||
}
|
||||
if (top_state.line_present) {
|
||||
output_funcs.remove_first();
|
||||
top_state.line_present = false;
|
||||
}
|
||||
if (top_state.entry) {
|
||||
output_funcs.prepend(render_state_line(top_state));
|
||||
top_state.line_present = true;
|
||||
}
|
||||
}
|
||||
|
||||
function prepend(journal_entry) {
|
||||
var entry = format_entry(journal_entry);
|
||||
|
||||
if (entry_is_equal(top_state.entry, entry)) {
|
||||
top_state.count += 1;
|
||||
top_state.first_time = entry.time;
|
||||
} else {
|
||||
top_output();
|
||||
|
||||
if (top_state.entry) {
|
||||
if (entry.bootid != top_state.entry.bootid)
|
||||
output_funcs.prepend(output_funcs.render_reboot_separator());
|
||||
if (entry.day != top_state.entry.day)
|
||||
output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
|
||||
}
|
||||
|
||||
start_new_line();
|
||||
top_state.entry = entry;
|
||||
top_state.count = 1;
|
||||
top_state.first_time = top_state.last_time = entry.time;
|
||||
top_state.line_present = false;
|
||||
}
|
||||
}
|
||||
|
||||
function prepend_flush() {
|
||||
top_output();
|
||||
if (top_state.entry) {
|
||||
output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
|
||||
top_state.header_present = true;
|
||||
}
|
||||
}
|
||||
|
||||
function bottom_output() {
|
||||
if (bottom_state.line_present) {
|
||||
output_funcs.remove_last();
|
||||
bottom_state.line_present = false;
|
||||
}
|
||||
if (bottom_state.entry) {
|
||||
output_funcs.append(render_state_line(bottom_state));
|
||||
bottom_state.line_present = true;
|
||||
}
|
||||
}
|
||||
|
||||
function append(journal_entry) {
|
||||
var entry = format_entry(journal_entry);
|
||||
|
||||
if (entry_is_equal(bottom_state.entry, entry)) {
|
||||
bottom_state.count += 1;
|
||||
bottom_state.last_time = entry.time;
|
||||
} else {
|
||||
bottom_output();
|
||||
|
||||
if (!bottom_state.entry || entry.day != bottom_state.entry.day) {
|
||||
output_funcs.append(output_funcs.render_day_header(entry.day));
|
||||
bottom_state.header_present = true;
|
||||
}
|
||||
if (bottom_state.entry && entry.bootid != bottom_state.entry.bootid)
|
||||
output_funcs.append(output_funcs.render_reboot_separator());
|
||||
|
||||
start_new_line();
|
||||
bottom_state.entry = entry;
|
||||
bottom_state.count = 1;
|
||||
bottom_state.first_time = bottom_state.last_time = entry.time;
|
||||
bottom_state.line_present = false;
|
||||
}
|
||||
}
|
||||
|
||||
function append_flush() {
|
||||
bottom_output();
|
||||
}
|
||||
|
||||
return { prepend: prepend,
|
||||
prepend_flush: prepend_flush,
|
||||
append: append,
|
||||
append_flush: append_flush
|
||||
};
|
||||
};
|
||||
|
||||
journal.logbox = function logbox(match, max_entries) {
|
||||
var entries = [ ];
|
||||
var box = document.createElement("div");
|
||||
|
||||
function render() {
|
||||
var renderer = journal.renderer(box);
|
||||
while(box.firstChild)
|
||||
box.removeChild(box.firstChild);
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
renderer.prepend(entries[i]);
|
||||
}
|
||||
renderer.prepend_flush();
|
||||
if (entries.length > 0)
|
||||
box.removeAttribute("hidden");
|
||||
else
|
||||
box.setAttribute("hidden", "hidden");
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
var promise = journal.journalctl(match, { count: max_entries }).
|
||||
stream(function(tail) {
|
||||
entries = entries.concat(tail);
|
||||
if (entries.length > max_entries)
|
||||
entries = entries.slice(-max_entries);
|
||||
render();
|
||||
}).
|
||||
fail(function(error) {
|
||||
box.appendChild(document.createTextNode(error.message));
|
||||
box.removeAttribute("hidden");
|
||||
});
|
||||
|
||||
/* Both a DOM element and a promise */
|
||||
return promise.promise(box);
|
||||
};
|
||||
|
||||
module.exports = journal;
|
||||
}());
|
||||
1
src/pkg/lib/journal_day_header.mustache
Normal file
1
src/pkg/lib/journal_day_header.mustache
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div class="panel-heading">{{day}}</div>
|
||||
19
src/pkg/lib/journal_line.mustache
Normal file
19
src/pkg/lib/journal_line.mustache
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<div class="cockpit-logline" data-cursor="{{cursor}}" role="row">
|
||||
<div class="cockpit-log-warning" role="cell">{{#warning}}
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
{{/warning}}{{#problem}}
|
||||
<i class="fa fa-times-circle-o"></i>
|
||||
{{/problem}}
|
||||
</div>
|
||||
<div class="cockpit-log-time" role="cell">{{time}}</div>
|
||||
<span class="cockpit-log-message" role="cell">{{message}}</span>
|
||||
{{! if we have count (repeated messages), show service name and badge - otherwise just the service }}
|
||||
{{#count}}
|
||||
<div class="cockpit-log-service-container" role="cell">
|
||||
<div class="cockpit-log-service-reduced">{{service}}</div>
|
||||
<span class="badge">{{count}} <i class="fa fa-caret-right"></i></span>
|
||||
</div>
|
||||
{{/count}}{{^count}}
|
||||
<div class="cockpit-log-service" role="cell">{{service}}</div>
|
||||
{{/count}}
|
||||
</div>
|
||||
5
src/pkg/lib/journal_reboot.mustache
Normal file
5
src/pkg/lib/journal_reboot.mustache
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<div class="cockpit-logline" role="row">
|
||||
{{! placeholders for correct message alignment }}
|
||||
<div class="cockpit-log-warning" role="cell"></div>
|
||||
<span class="cockpit-log-message cockpit-logmsg-reboot" role="cell">{{message}}</span>
|
||||
</div>
|
||||
502
src/pkg/lib/listing.less
Normal file
502
src/pkg/lib/listing.less
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
/* Listing pattern */
|
||||
|
||||
@import "./variables.less";
|
||||
|
||||
table.listing-ct {
|
||||
margin-top: @listing-ct-spacing;
|
||||
min-width: 65%;
|
||||
}
|
||||
|
||||
table.listing-ct > caption,
|
||||
table.listing-ct > thead h3 {
|
||||
font-size: @font-size-h2;
|
||||
padding: @listing-ct-padding 0px @listing-ct-padding;
|
||||
font-weight: 300;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
table.listing-ct > caption {
|
||||
color: inherit;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
table.listing-ct > thead td {
|
||||
padding-top: @listing-ct-padding * 2;
|
||||
}
|
||||
|
||||
table.listing-ct > thead:first-child td {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
table.listing-ct > thead td > a {
|
||||
line-height: 30px;
|
||||
padding: @listing-ct-padding 0 @listing-ct-padding;
|
||||
}
|
||||
|
||||
table.listing-ct > thead th {
|
||||
border-top: 1px solid @gray-lighter;
|
||||
font-weight: normal;
|
||||
padding: @listing-ct-padding;
|
||||
color: @listing-ct-metadata;
|
||||
}
|
||||
|
||||
table.listing-ct > thead th:first-child {
|
||||
padding-left: @listing-ct-padding * 2;
|
||||
}
|
||||
|
||||
table.listing-ct > thead th:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* A listing item is a row in the table */
|
||||
tbody > tr.listing-ct-item {
|
||||
border-top: 1px solid @gray-lighter;
|
||||
border-bottom: 1px solid @gray-lighter;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table.listing-ct > tbody:last-child {
|
||||
border-bottom: 1px solid @gray-lighter;
|
||||
}
|
||||
table.listing-ct > tbody + thead {
|
||||
border-top: 1px solid @gray-lighter;
|
||||
}
|
||||
table.listing-ct > tbody.open:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
table.listing-ct > tbody.open + thead {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
tbody > tr.listing-ct-item.listing-ct-warning {
|
||||
background-color: @listing-ct-warning-color;
|
||||
}
|
||||
|
||||
tbody.open > tr.listing-ct-item {
|
||||
background-color: @color-pf-black-200;
|
||||
border-bottom: none;
|
||||
border-top: none;
|
||||
border-left: 1px solid @listing-ct-border;
|
||||
border-right: 1px solid @listing-ct-border;
|
||||
}
|
||||
|
||||
tbody.open > tr.listing-ct-item td,
|
||||
tbody.open > tr.listing-ct-item th {
|
||||
border-top: 1px solid @listing-ct-border;
|
||||
}
|
||||
|
||||
tbody.open td div.listing-ct-head {
|
||||
background-color: @color-pf-white;
|
||||
}
|
||||
|
||||
tbody.open .listing-ct-panel {
|
||||
border: 1px solid @listing-ct-border;
|
||||
}
|
||||
|
||||
tbody.open .listing-ct-panel .listing-ct-body {
|
||||
border: none;
|
||||
}
|
||||
|
||||
tbody.open > tr.listing-ct-panel + tr.listing-ct-body {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
tbody.open > tr.listing-ct-panel td div.listing-ct-head {
|
||||
border: none;
|
||||
border-bottom: 1px solid @listing-ct-border;
|
||||
}
|
||||
|
||||
/* only highlight the row if navigation is available */
|
||||
tbody:not(.open) > tr.listing-ct-item:not(.listing-ct-nonavigate):hover {
|
||||
background-color: @listing-ct-hover;
|
||||
}
|
||||
|
||||
/* if we can't navigate to a row but expand is available, highlight the caret */
|
||||
tbody:not(.open) > tr.listing-ct-item.listing-ct-nonavigate:hover td.listing-ct-toggle {
|
||||
color: @listing-ct-active;
|
||||
}
|
||||
|
||||
/* use gray for a row that's expanded or if navigation isn't available */
|
||||
tbody.open > tr.listing-ct-item:hover,
|
||||
tr.listing-ct-item.listing-ct-nonavigate:hover {
|
||||
background-color: @color-pf-black-200;
|
||||
}
|
||||
|
||||
/* always highlight caret when hovering over an expanded row */
|
||||
tbody.open > tr.listing-ct-item:hover td.listing-ct-toggle {
|
||||
color: @listing-ct-active;
|
||||
}
|
||||
|
||||
tr.listing-ct-item .listing-ct-toggle {
|
||||
padding: 0 !important;
|
||||
width: 35px;
|
||||
color: @color-pf-black;
|
||||
}
|
||||
|
||||
table.listing-ct thead .listing-ct-toggle + th,
|
||||
tr.listing-ct-item .listing-ct-toggle + td,
|
||||
tr.listing-ct-item .listing-ct-toggle + th {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
tr:not(.listing-ct-selected) {
|
||||
td.listing-ct-toggle:hover {
|
||||
color: @listing-ct-active;
|
||||
background-color: @color-pf-black-200;
|
||||
|
||||
~ td, ~ th {
|
||||
background-color: @color-pf-black-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
td.listing-ct-toggle i {
|
||||
font-size: 24px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
tr.listing-ct-item:hover td.listing-ct-toggle i,
|
||||
tr.listing-ct-item td.listing-ct-toggle:hover i {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
td.listing-ct-toggle i:before {
|
||||
content: "\f105";
|
||||
}
|
||||
|
||||
tbody.open td.listing-ct-toggle i {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
tbody.open td.listing-ct-toggle i:before {
|
||||
content: "\f107";
|
||||
}
|
||||
|
||||
/* Listing items have decent padding ... */
|
||||
tr.listing-ct-item td {
|
||||
padding: @listing-ct-padding;
|
||||
}
|
||||
|
||||
tr.listing-ct-item th {
|
||||
padding: @listing-ct-padding @listing-ct-padding @listing-ct-padding @listing-ct-spacing;
|
||||
}
|
||||
|
||||
/* Listing caption is text next to the actions, text should be similar to nav (.nav-tabs-pf > li > a)*/
|
||||
.listing-ct-actions > .listing-ct-caption {
|
||||
font-size: @font-size-h5;
|
||||
vertical-align: middle;
|
||||
color: @link-color;
|
||||
padding-right: @listing-ct-padding / 2;
|
||||
}
|
||||
|
||||
/* Listing actions can be used directly as a cell */
|
||||
tr.listing-ct-item td.listing-ct-actions,
|
||||
td.listing-ct-actions {
|
||||
padding: @listing-ct-padding / 2 @listing-ct-padding;
|
||||
text-align: right;
|
||||
float: none;
|
||||
}
|
||||
|
||||
/* if the entire row is selected, highlight */
|
||||
tr.listing-ct-item.listing-ct-selected {
|
||||
background-color: @color-pf-blue-400;
|
||||
color: @color-pf-white;
|
||||
border-color: multiply(@color-pf-black-200, @color-pf-blue-400);
|
||||
|
||||
&:hover {
|
||||
background-color: multiply(@color-pf-black-200, @color-pf-blue-400);
|
||||
border-color: multiply(@color-pf-black-200, @color-pf-blue-400);
|
||||
}
|
||||
}
|
||||
|
||||
tr.listing-ct-item.listing-ct-selected .badge {
|
||||
background-color: multiply(@badge-bg, @color-pf-blue-400);
|
||||
|
||||
&:hover {
|
||||
background-color: multiply(@badge-bg, @color-pf-blue-500);
|
||||
}
|
||||
}
|
||||
|
||||
.listing-ct-head .listing-ct-actions {
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
tr.listing-ct-item td:first-child {
|
||||
padding-left: @listing-ct-padding * 2;
|
||||
}
|
||||
|
||||
/* The last column of a listing is always right aligned */
|
||||
tr.listing-ct-item td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.listing-ct-panel {
|
||||
box-shadow: 1px 1px 1px 1px @listing-ct-open;
|
||||
margin-bottom: @listing-ct-spacing;
|
||||
}
|
||||
|
||||
div.listing-ct-maybe {
|
||||
border: 1px dashed @listing-ct-border-maybe;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
div.listing-ct-head {
|
||||
padding: @listing-ct-padding @listing-ct-padding 0 @listing-ct-padding;
|
||||
background-color: @listing-ct-open;
|
||||
border-color: @listing-ct-border;
|
||||
border-style: solid;
|
||||
border-width: 1px 1px 0 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.listing-ct-head:last-child {
|
||||
padding-bottom: @listing-ct-padding;
|
||||
}
|
||||
|
||||
div.listing-ct-maybe div.listing-ct-head,
|
||||
div.listing-ct-maybe div.listing-ct-body {
|
||||
background-color: @color-pf-white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
tbody.active .listing-ct-head {
|
||||
border-top: @listing-ct-open-width solid #0099d3;
|
||||
}
|
||||
|
||||
.listing-ct-head h3 {
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
margin-top: 0px;
|
||||
margin-left: @listing-ct-padding / 2;
|
||||
margin-bottom: @listing-ct-padding;
|
||||
}
|
||||
|
||||
.listing-ct-head h3 i {
|
||||
float: left;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
.listing-ct-head .nav li a {
|
||||
padding-top: 0px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.listing-ct-head .nav-tabs {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.listing-ct-head .nav-tabs-pf {
|
||||
margin-left: -@listing-ct-padding;
|
||||
}
|
||||
|
||||
/* To display info instead of tabs */
|
||||
.listing-ct-head dl {
|
||||
display: inline-block;
|
||||
height: 1.6em;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
margin-right: 45px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.listing-ct-head dt {
|
||||
font-weight: normal;
|
||||
display: inline;
|
||||
margin-right: 0.5em;
|
||||
color: @listing-ct-metadata;
|
||||
}
|
||||
|
||||
.listing-ct-head dd {
|
||||
display: inline;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.listing-ct-body {
|
||||
padding: @listing-ct-padding * 2 @listing-ct-padding + @listing-ct-spacing;
|
||||
font-size: 13px;
|
||||
border: 1px solid @listing-ct-border;
|
||||
background-color: @color-pf-white;
|
||||
}
|
||||
|
||||
.listing-ct-inline > .listing-ct-body {
|
||||
border: none;
|
||||
padding-top: 0px;
|
||||
padding-left: @listing-ct-padding * 2;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.listing-ct-inline > h3 {
|
||||
border-top: 1px solid @listing-ct-border;
|
||||
padding-top: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.listing-ct-inline > h3:first-child {
|
||||
border-top: none;
|
||||
padding-top: 0px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.listing-ct-actions {
|
||||
float: right;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
.listing-ct-status {
|
||||
float: right;
|
||||
clear: right;
|
||||
}
|
||||
|
||||
.listing-ct-error {
|
||||
border-top: 1px solid @listing-ct-border;
|
||||
border-left: 1px solid @listing-ct-border;
|
||||
border-right: 1px solid @listing-ct-border;
|
||||
}
|
||||
|
||||
.listing-ct-error.alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.listing-ct-body tt {
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
.listing-ct-body dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.listing-ct-body dl dd dl.inline-dl dt,
|
||||
.listing-ct-body dt {
|
||||
clear: left;
|
||||
float: left;
|
||||
width: 100px;
|
||||
min-height: 26px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: @listing-ct-metadata;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.listing-ct-body dt {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.listing-ct-body dl dd dl dt {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.listing-ct-body dd {
|
||||
margin-left: 110px;
|
||||
min-height: 26px;
|
||||
max-width: 1000px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.listing-ct-body dl.full-width dt {
|
||||
text-align: left;
|
||||
min-width: none;
|
||||
max-width: none;
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.listing-ct-body dl.full-width dd {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
table.listing-ct tbody.open {
|
||||
box-shadow: 1px 1px 1px 1px @color-pf-black-150;
|
||||
}
|
||||
|
||||
/* By default these thincgs are hidden */
|
||||
tbody tr.listing-ct-panel,
|
||||
tbody tr.listing-ct-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ... unless they are in the right state */
|
||||
tbody.open tr.listing-ct-head,
|
||||
tbody.open tr.listing-ct-panel,
|
||||
tbody.open tr.listing-ct-body {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
tbody.open tr.listing-ct-head {
|
||||
border-color: @listing-ct-border;
|
||||
border-style: solid;
|
||||
border-width: 1px 1px 0 1px;
|
||||
border-top: @listing-ct-open-width solid #0099d3;
|
||||
background-color: @listing-ct-open;
|
||||
}
|
||||
|
||||
tr.listing-ct-head + tr.listing-ct-head {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
tr.listing-ct-head + tr.listing-ct-head td {
|
||||
padding: 0px @listing-ct-padding 0px @listing-ct-padding;
|
||||
}
|
||||
|
||||
tr.listing-ct-body td {
|
||||
padding: @listing-ct-padding * 2 @listing-ct-padding + @listing-ct-spacing;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.listing-ct-empty {
|
||||
color: #888;
|
||||
text-align: center;
|
||||
border-top: 1px solid @listing-ct-border-light;
|
||||
border-bottom: 1px solid @listing-ct-border-light;
|
||||
}
|
||||
|
||||
/* Used at the end of a group of tbody to show an 'empty' message */
|
||||
thead.listing-ct-empty td,
|
||||
table.listing-ct > thead.listing-ct-empty td {
|
||||
padding: @listing-ct-padding;
|
||||
}
|
||||
|
||||
tbody + thead.listing-ct-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Listing pattern defaults to using full width of parent */
|
||||
.listing-ct-wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.listing-ct-head {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.listing-ct-head .btn-group,
|
||||
.listing-ct-head button:not(.dropdown-toggle) {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
table.listing-ct > caption a {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
tbody tr.listing-ct-noexpand {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Fix up nav-tabs-pf to work properly */
|
||||
|
||||
.nav-tabs-pf > li:first-child a {
|
||||
margin-left: 0px !important;
|
||||
padding-left: @listing-ct-spacing !important;
|
||||
}
|
||||
|
||||
.nav-tabs-pf > li a:before {
|
||||
right: 0px !important;
|
||||
}
|
||||
|
||||
.nav-tabs-pf > li > a:active:before,
|
||||
.nav-tabs-pf > li > a:focus:before,
|
||||
.nav-tabs-pf > li > a:hover:before,
|
||||
.nav-tabs-pf > li.active a:before {
|
||||
height: @listing-ct-open-width;
|
||||
left: 0px;
|
||||
}
|
||||
235
src/pkg/lib/page.css
Normal file
235
src/pkg/lib/page.css
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
min-height: 26px;
|
||||
min-width: 26px;
|
||||
}
|
||||
|
||||
.btn.disabled {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.btn.disabled:hover {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
a.disabled {
|
||||
cursor: not-allowed !important;
|
||||
text-decoration: none;
|
||||
pointer-events: none;
|
||||
color: #8b8d8f;
|
||||
}
|
||||
|
||||
a.disabled:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-menu > li > a.disabled,
|
||||
.dropdown-menu > li > a.disabled:hover,
|
||||
.dropdown-menu > li > a.disabled:focus {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.dropdown-menu > li > a.disabled:hover,
|
||||
.dropdown-menu > li > a.disabled:focus {
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border-color: transparent;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Limit dropdown menus to 90% of the viewport size */
|
||||
.dropdown-menu {
|
||||
height: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 90vh;
|
||||
}
|
||||
/* Align these buttons more nicely */
|
||||
.btn.fa-minus,
|
||||
.btn.fa-plus {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* HACK: Workaround for https://github.com/patternfly/patternfly/issues/174*/
|
||||
|
||||
.page-ct {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.highlight-ct {
|
||||
background-color: #d4edfa;
|
||||
}
|
||||
|
||||
/* Well and Blankslate */
|
||||
|
||||
.curtains-ct {
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.panel .well {
|
||||
margin-bottom: 0px;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
.well.blank-slate-pf {
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.05) inset;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.blank-slate-pf .spinner-lg {
|
||||
height: 58px;
|
||||
width: 58px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Control buttons such as play and stop
|
||||
* Used with .btn .btn-default
|
||||
*/
|
||||
|
||||
.btn-control-ct,
|
||||
.btn-control-ct:hover {
|
||||
background-position: center center;
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: none;
|
||||
-webkit-border-radius: 2;
|
||||
-moz-border-radius: 2;
|
||||
border-radius: 2px;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-control-ct {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.btn-control-ct:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
|
||||
/* On/off switch */
|
||||
|
||||
.btn-onoff-ct {
|
||||
margin: 1px 0px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn {
|
||||
color: transparent;
|
||||
border-color: #B7B7B7;
|
||||
padding: 2px 6px 1px 6px;
|
||||
background-color: white;
|
||||
background-image: linear-gradient(to bottom, rgb(250, 250, 250) 0px, rgb(237, 237, 237) 100%);
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
width: 37px;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn:first-child {
|
||||
border-right: #00435F;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn:last-child {
|
||||
border-left: #00435F;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn.active {
|
||||
background-image: none;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn.active:first-child {
|
||||
background-color: #0086CF;
|
||||
color: white;
|
||||
border-right: 1px solid #0071b0;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn.active:last-child {
|
||||
color: #000;
|
||||
border-left: 1px solid #d6d6d6;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn.disabled {
|
||||
pointer-events: none;
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
.btn-onoff-ct .btn.active.disabled {
|
||||
background-color: #888 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Small list inside a dialog */
|
||||
/* Alert fixups */
|
||||
|
||||
/* HACK: word-wrap workaround for long alerts https://github.com/patternfly/patternfly/issues/491 */
|
||||
|
||||
.modal-content .alert {
|
||||
text-align: left;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.modal-content .alert .fa {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modal-content .alert .pficon {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.alert.alert-danger .fa {
|
||||
color: #af151a;
|
||||
}
|
||||
|
||||
/* Dialog patterns */
|
||||
|
||||
.dialog-wait-ct {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.dialog-wait-ct .spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dialog-wait-ct span {
|
||||
vertical-align: 4px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.dialog-list-ct {
|
||||
max-height: 230px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #CCC;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* HACK: https://github.com/patternfly/patternfly/issues/255 */
|
||||
input[type=number] {
|
||||
padding: 0 0 0 5px;
|
||||
}
|
||||
|
||||
/* Make a dialog visible */
|
||||
.dialog-ct-visible {
|
||||
display: block;
|
||||
}
|
||||
146
src/pkg/lib/table.css
Normal file
146
src/pkg/lib/table.css
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/* Panels don't draw borders between them */
|
||||
.panel > .table > tbody:first-child td {
|
||||
border-top: 1px solid rgb(221, 221, 221);
|
||||
}
|
||||
|
||||
/* Table headers should not generate a double border */
|
||||
.panel .table thead tr th {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
background: #F5F5F5;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/* Vertically center dropdown buttons in panel headers */
|
||||
.panel-heading .btn {
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fix up table row hovering.
|
||||
*
|
||||
* When you hover over table rows it's because they're clickable.
|
||||
* Make the table row hover color match the list-group-item.
|
||||
*/
|
||||
.table-hover > tbody > tr > td,
|
||||
.table-hover > tbody > tr > th,
|
||||
.dialog-list-ct .list-group-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.table-hover > tbody > tr:hover > td,
|
||||
.table-hover > tbody > tr:hover > th,
|
||||
.dialog-list-ct .list-group-item:hover:not(.active) {
|
||||
background-color: #d4edfa;
|
||||
}
|
||||
|
||||
/* Override patternfly to fit buttons and such */
|
||||
.table > thead > tr > th,
|
||||
.table > tbody > tr > td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Override the heavy patternfly headers */
|
||||
.table > thead {
|
||||
background-image: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Make things line up */
|
||||
.table tbody tr td:first-child,
|
||||
.table thead tr th:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.table tbody tr td:last-child,
|
||||
.table thead tr th:last-child {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.info-table-ct > tr > td,
|
||||
.info-table-ct > tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 0.75em;
|
||||
padding-top: 0.25em;
|
||||
vertical-align: top;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.info-table-ct > tr > td:first-child,
|
||||
.info-table-ct > tbody > tr > td:first-child {
|
||||
text-align: right;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.info-table-ct > tr > td:not(:first-child),
|
||||
.info-table-ct > tbody > tr > td:not(:first-child) {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.info-table-ct > tr > td button,
|
||||
.info-table-ct > tbody > tr > td button {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.form-table-ct {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-table-ct td {
|
||||
padding-left: 0.75em;
|
||||
padding-top: 0.25em;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.form-table-ct td.top {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.form-table-ct td:first-child {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
color: #888888;
|
||||
width: 5px; /* will be expanded by nowrap */
|
||||
}
|
||||
|
||||
.form-table-ct td[colspan] {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
.form-table-ct td {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.form-table-ct td.header {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
color: #4D5258;
|
||||
padding: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.form-table-ct label input[type='radio'],
|
||||
.form-table-ct label input[type='checkbox'] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.form-table-ct label {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.form-table-ct label span {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
/* Break up sidebar in columns in smaller sizes*/
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.info-table-ct-container .info-table-ct {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
522
src/pkg/lib/term.css
Normal file
522
src/pkg/lib/term.css
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
.term-bg-color-0 { background-color: #2e3436; }
|
||||
.term-fg-color-0 { color: #2e3436; }
|
||||
.term-bg-color-1 { background-color: #cc0000; }
|
||||
.term-fg-color-1 { color: #cc0000; }
|
||||
.term-bg-color-2 { background-color: #4e9a06; }
|
||||
.term-fg-color-2 { color: #4e9a06; }
|
||||
.term-bg-color-3 { background-color: #c4a000; }
|
||||
.term-fg-color-3 { color: #c4a000; }
|
||||
.term-bg-color-4 { background-color: #3465a4; }
|
||||
.term-fg-color-4 { color: #3465a4; }
|
||||
.term-bg-color-5 { background-color: #75507b; }
|
||||
.term-fg-color-5 { color: #75507b; }
|
||||
.term-bg-color-6 { background-color: #06989a; }
|
||||
.term-fg-color-6 { color: #06989a; }
|
||||
.term-bg-color-7 { background-color: #d3d7cf; }
|
||||
.term-fg-color-7 { color: #d3d7cf; }
|
||||
.term-bg-color-8 { background-color: #555753; }
|
||||
.term-fg-color-8 { color: #555753; }
|
||||
.term-bg-color-9 { background-color: #ef2929; }
|
||||
.term-fg-color-9 { color: #ef2929; }
|
||||
.term-bg-color-10 { background-color: #8ae234; }
|
||||
.term-fg-color-10 { color: #8ae234; }
|
||||
.term-bg-color-11 { background-color: #fce94f; }
|
||||
.term-fg-color-11 { color: #fce94f; }
|
||||
.term-bg-color-12 { background-color: #729fcf; }
|
||||
.term-fg-color-12 { color: #729fcf; }
|
||||
.term-bg-color-13 { background-color: #ad7fa8; }
|
||||
.term-fg-color-13 { color: #ad7fa8; }
|
||||
.term-bg-color-14 { background-color: #34e2e2; }
|
||||
.term-fg-color-14 { color: #34e2e2; }
|
||||
.term-bg-color-15 { background-color: #eeeeec; }
|
||||
.term-fg-color-15 { color: #eeeeec; }
|
||||
.term-bg-color-16 { background-color: #000000; }
|
||||
.term-fg-color-16 { color: #000000; }
|
||||
.term-bg-color-17 { background-color: #00005f; }
|
||||
.term-fg-color-17 { color: #00005f; }
|
||||
.term-bg-color-18 { background-color: #000087; }
|
||||
.term-fg-color-18 { color: #000087; }
|
||||
.term-bg-color-19 { background-color: #0000af; }
|
||||
.term-fg-color-19 { color: #0000af; }
|
||||
.term-bg-color-20 { background-color: #0000d7; }
|
||||
.term-fg-color-20 { color: #0000d7; }
|
||||
.term-bg-color-21 { background-color: #0000ff; }
|
||||
.term-fg-color-21 { color: #0000ff; }
|
||||
.term-bg-color-22 { background-color: #005f00; }
|
||||
.term-fg-color-22 { color: #005f00; }
|
||||
.term-bg-color-23 { background-color: #005f5f; }
|
||||
.term-fg-color-23 { color: #005f5f; }
|
||||
.term-bg-color-24 { background-color: #005f87; }
|
||||
.term-fg-color-24 { color: #005f87; }
|
||||
.term-bg-color-25 { background-color: #005faf; }
|
||||
.term-fg-color-25 { color: #005faf; }
|
||||
.term-bg-color-26 { background-color: #005fd7; }
|
||||
.term-fg-color-26 { color: #005fd7; }
|
||||
.term-bg-color-27 { background-color: #005fff; }
|
||||
.term-fg-color-27 { color: #005fff; }
|
||||
.term-bg-color-28 { background-color: #008700; }
|
||||
.term-fg-color-28 { color: #008700; }
|
||||
.term-bg-color-29 { background-color: #00875f; }
|
||||
.term-fg-color-29 { color: #00875f; }
|
||||
.term-bg-color-30 { background-color: #008787; }
|
||||
.term-fg-color-30 { color: #008787; }
|
||||
.term-bg-color-31 { background-color: #0087af; }
|
||||
.term-fg-color-31 { color: #0087af; }
|
||||
.term-bg-color-32 { background-color: #0087d7; }
|
||||
.term-fg-color-32 { color: #0087d7; }
|
||||
.term-bg-color-33 { background-color: #0087ff; }
|
||||
.term-fg-color-33 { color: #0087ff; }
|
||||
.term-bg-color-34 { background-color: #00af00; }
|
||||
.term-fg-color-34 { color: #00af00; }
|
||||
.term-bg-color-35 { background-color: #00af5f; }
|
||||
.term-fg-color-35 { color: #00af5f; }
|
||||
.term-bg-color-36 { background-color: #00af87; }
|
||||
.term-fg-color-36 { color: #00af87; }
|
||||
.term-bg-color-37 { background-color: #00afaf; }
|
||||
.term-fg-color-37 { color: #00afaf; }
|
||||
.term-bg-color-38 { background-color: #00afd7; }
|
||||
.term-fg-color-38 { color: #00afd7; }
|
||||
.term-bg-color-39 { background-color: #00afff; }
|
||||
.term-fg-color-39 { color: #00afff; }
|
||||
.term-bg-color-40 { background-color: #00d700; }
|
||||
.term-fg-color-40 { color: #00d700; }
|
||||
.term-bg-color-41 { background-color: #00d75f; }
|
||||
.term-fg-color-41 { color: #00d75f; }
|
||||
.term-bg-color-42 { background-color: #00d787; }
|
||||
.term-fg-color-42 { color: #00d787; }
|
||||
.term-bg-color-43 { background-color: #00d7af; }
|
||||
.term-fg-color-43 { color: #00d7af; }
|
||||
.term-bg-color-44 { background-color: #00d7d7; }
|
||||
.term-fg-color-44 { color: #00d7d7; }
|
||||
.term-bg-color-45 { background-color: #00d7ff; }
|
||||
.term-fg-color-45 { color: #00d7ff; }
|
||||
.term-bg-color-46 { background-color: #00ff00; }
|
||||
.term-fg-color-46 { color: #00ff00; }
|
||||
.term-bg-color-47 { background-color: #00ff5f; }
|
||||
.term-fg-color-47 { color: #00ff5f; }
|
||||
.term-bg-color-48 { background-color: #00ff87; }
|
||||
.term-fg-color-48 { color: #00ff87; }
|
||||
.term-bg-color-49 { background-color: #00ffaf; }
|
||||
.term-fg-color-49 { color: #00ffaf; }
|
||||
.term-bg-color-50 { background-color: #00ffd7; }
|
||||
.term-fg-color-50 { color: #00ffd7; }
|
||||
.term-bg-color-51 { background-color: #00ffff; }
|
||||
.term-fg-color-51 { color: #00ffff; }
|
||||
.term-bg-color-52 { background-color: #5f0000; }
|
||||
.term-fg-color-52 { color: #5f0000; }
|
||||
.term-bg-color-53 { background-color: #5f005f; }
|
||||
.term-fg-color-53 { color: #5f005f; }
|
||||
.term-bg-color-54 { background-color: #5f0087; }
|
||||
.term-fg-color-54 { color: #5f0087; }
|
||||
.term-bg-color-55 { background-color: #5f00af; }
|
||||
.term-fg-color-55 { color: #5f00af; }
|
||||
.term-bg-color-56 { background-color: #5f00d7; }
|
||||
.term-fg-color-56 { color: #5f00d7; }
|
||||
.term-bg-color-57 { background-color: #5f00ff; }
|
||||
.term-fg-color-57 { color: #5f00ff; }
|
||||
.term-bg-color-58 { background-color: #5f5f00; }
|
||||
.term-fg-color-58 { color: #5f5f00; }
|
||||
.term-bg-color-59 { background-color: #5f5f5f; }
|
||||
.term-fg-color-59 { color: #5f5f5f; }
|
||||
.term-bg-color-60 { background-color: #5f5f87; }
|
||||
.term-fg-color-60 { color: #5f5f87; }
|
||||
.term-bg-color-61 { background-color: #5f5faf; }
|
||||
.term-fg-color-61 { color: #5f5faf; }
|
||||
.term-bg-color-62 { background-color: #5f5fd7; }
|
||||
.term-fg-color-62 { color: #5f5fd7; }
|
||||
.term-bg-color-63 { background-color: #5f5fff; }
|
||||
.term-fg-color-63 { color: #5f5fff; }
|
||||
.term-bg-color-64 { background-color: #5f8700; }
|
||||
.term-fg-color-64 { color: #5f8700; }
|
||||
.term-bg-color-65 { background-color: #5f875f; }
|
||||
.term-fg-color-65 { color: #5f875f; }
|
||||
.term-bg-color-66 { background-color: #5f8787; }
|
||||
.term-fg-color-66 { color: #5f8787; }
|
||||
.term-bg-color-67 { background-color: #5f87af; }
|
||||
.term-fg-color-67 { color: #5f87af; }
|
||||
.term-bg-color-68 { background-color: #5f87d7; }
|
||||
.term-fg-color-68 { color: #5f87d7; }
|
||||
.term-bg-color-69 { background-color: #5f87ff; }
|
||||
.term-fg-color-69 { color: #5f87ff; }
|
||||
.term-bg-color-70 { background-color: #5faf00; }
|
||||
.term-fg-color-70 { color: #5faf00; }
|
||||
.term-bg-color-71 { background-color: #5faf5f; }
|
||||
.term-fg-color-71 { color: #5faf5f; }
|
||||
.term-bg-color-72 { background-color: #5faf87; }
|
||||
.term-fg-color-72 { color: #5faf87; }
|
||||
.term-bg-color-73 { background-color: #5fafaf; }
|
||||
.term-fg-color-73 { color: #5fafaf; }
|
||||
.term-bg-color-74 { background-color: #5fafd7; }
|
||||
.term-fg-color-74 { color: #5fafd7; }
|
||||
.term-bg-color-75 { background-color: #5fafff; }
|
||||
.term-fg-color-75 { color: #5fafff; }
|
||||
.term-bg-color-76 { background-color: #5fd700; }
|
||||
.term-fg-color-76 { color: #5fd700; }
|
||||
.term-bg-color-77 { background-color: #5fd75f; }
|
||||
.term-fg-color-77 { color: #5fd75f; }
|
||||
.term-bg-color-78 { background-color: #5fd787; }
|
||||
.term-fg-color-78 { color: #5fd787; }
|
||||
.term-bg-color-79 { background-color: #5fd7af; }
|
||||
.term-fg-color-79 { color: #5fd7af; }
|
||||
.term-bg-color-80 { background-color: #5fd7d7; }
|
||||
.term-fg-color-80 { color: #5fd7d7; }
|
||||
.term-bg-color-81 { background-color: #5fd7ff; }
|
||||
.term-fg-color-81 { color: #5fd7ff; }
|
||||
.term-bg-color-82 { background-color: #5fff00; }
|
||||
.term-fg-color-82 { color: #5fff00; }
|
||||
.term-bg-color-83 { background-color: #5fff5f; }
|
||||
.term-fg-color-83 { color: #5fff5f; }
|
||||
.term-bg-color-84 { background-color: #5fff87; }
|
||||
.term-fg-color-84 { color: #5fff87; }
|
||||
.term-bg-color-85 { background-color: #5fffaf; }
|
||||
.term-fg-color-85 { color: #5fffaf; }
|
||||
.term-bg-color-86 { background-color: #5fffd7; }
|
||||
.term-fg-color-86 { color: #5fffd7; }
|
||||
.term-bg-color-87 { background-color: #5fffff; }
|
||||
.term-fg-color-87 { color: #5fffff; }
|
||||
.term-bg-color-88 { background-color: #870000; }
|
||||
.term-fg-color-88 { color: #870000; }
|
||||
.term-bg-color-89 { background-color: #87005f; }
|
||||
.term-fg-color-89 { color: #87005f; }
|
||||
.term-bg-color-90 { background-color: #870087; }
|
||||
.term-fg-color-90 { color: #870087; }
|
||||
.term-bg-color-91 { background-color: #8700af; }
|
||||
.term-fg-color-91 { color: #8700af; }
|
||||
.term-bg-color-92 { background-color: #8700d7; }
|
||||
.term-fg-color-92 { color: #8700d7; }
|
||||
.term-bg-color-93 { background-color: #8700ff; }
|
||||
.term-fg-color-93 { color: #8700ff; }
|
||||
.term-bg-color-94 { background-color: #875f00; }
|
||||
.term-fg-color-94 { color: #875f00; }
|
||||
.term-bg-color-95 { background-color: #875f5f; }
|
||||
.term-fg-color-95 { color: #875f5f; }
|
||||
.term-bg-color-96 { background-color: #875f87; }
|
||||
.term-fg-color-96 { color: #875f87; }
|
||||
.term-bg-color-97 { background-color: #875faf; }
|
||||
.term-fg-color-97 { color: #875faf; }
|
||||
.term-bg-color-98 { background-color: #875fd7; }
|
||||
.term-fg-color-98 { color: #875fd7; }
|
||||
.term-bg-color-99 { background-color: #875fff; }
|
||||
.term-fg-color-99 { color: #875fff; }
|
||||
.term-bg-color-100 { background-color: #878700; }
|
||||
.term-fg-color-100 { color: #878700; }
|
||||
.term-bg-color-101 { background-color: #87875f; }
|
||||
.term-fg-color-101 { color: #87875f; }
|
||||
.term-bg-color-102 { background-color: #878787; }
|
||||
.term-fg-color-102 { color: #878787; }
|
||||
.term-bg-color-103 { background-color: #8787af; }
|
||||
.term-fg-color-103 { color: #8787af; }
|
||||
.term-bg-color-104 { background-color: #8787d7; }
|
||||
.term-fg-color-104 { color: #8787d7; }
|
||||
.term-bg-color-105 { background-color: #8787ff; }
|
||||
.term-fg-color-105 { color: #8787ff; }
|
||||
.term-bg-color-106 { background-color: #87af00; }
|
||||
.term-fg-color-106 { color: #87af00; }
|
||||
.term-bg-color-107 { background-color: #87af5f; }
|
||||
.term-fg-color-107 { color: #87af5f; }
|
||||
.term-bg-color-108 { background-color: #87af87; }
|
||||
.term-fg-color-108 { color: #87af87; }
|
||||
.term-bg-color-109 { background-color: #87afaf; }
|
||||
.term-fg-color-109 { color: #87afaf; }
|
||||
.term-bg-color-110 { background-color: #87afd7; }
|
||||
.term-fg-color-110 { color: #87afd7; }
|
||||
.term-bg-color-111 { background-color: #87afff; }
|
||||
.term-fg-color-111 { color: #87afff; }
|
||||
.term-bg-color-112 { background-color: #87d700; }
|
||||
.term-fg-color-112 { color: #87d700; }
|
||||
.term-bg-color-113 { background-color: #87d75f; }
|
||||
.term-fg-color-113 { color: #87d75f; }
|
||||
.term-bg-color-114 { background-color: #87d787; }
|
||||
.term-fg-color-114 { color: #87d787; }
|
||||
.term-bg-color-115 { background-color: #87d7af; }
|
||||
.term-fg-color-115 { color: #87d7af; }
|
||||
.term-bg-color-116 { background-color: #87d7d7; }
|
||||
.term-fg-color-116 { color: #87d7d7; }
|
||||
.term-bg-color-117 { background-color: #87d7ff; }
|
||||
.term-fg-color-117 { color: #87d7ff; }
|
||||
.term-bg-color-118 { background-color: #87ff00; }
|
||||
.term-fg-color-118 { color: #87ff00; }
|
||||
.term-bg-color-119 { background-color: #87ff5f; }
|
||||
.term-fg-color-119 { color: #87ff5f; }
|
||||
.term-bg-color-120 { background-color: #87ff87; }
|
||||
.term-fg-color-120 { color: #87ff87; }
|
||||
.term-bg-color-121 { background-color: #87ffaf; }
|
||||
.term-fg-color-121 { color: #87ffaf; }
|
||||
.term-bg-color-122 { background-color: #87ffd7; }
|
||||
.term-fg-color-122 { color: #87ffd7; }
|
||||
.term-bg-color-123 { background-color: #87ffff; }
|
||||
.term-fg-color-123 { color: #87ffff; }
|
||||
.term-bg-color-124 { background-color: #af0000; }
|
||||
.term-fg-color-124 { color: #af0000; }
|
||||
.term-bg-color-125 { background-color: #af005f; }
|
||||
.term-fg-color-125 { color: #af005f; }
|
||||
.term-bg-color-126 { background-color: #af0087; }
|
||||
.term-fg-color-126 { color: #af0087; }
|
||||
.term-bg-color-127 { background-color: #af00af; }
|
||||
.term-fg-color-127 { color: #af00af; }
|
||||
.term-bg-color-128 { background-color: #af00d7; }
|
||||
.term-fg-color-128 { color: #af00d7; }
|
||||
.term-bg-color-129 { background-color: #af00ff; }
|
||||
.term-fg-color-129 { color: #af00ff; }
|
||||
.term-bg-color-130 { background-color: #af5f00; }
|
||||
.term-fg-color-130 { color: #af5f00; }
|
||||
.term-bg-color-131 { background-color: #af5f5f; }
|
||||
.term-fg-color-131 { color: #af5f5f; }
|
||||
.term-bg-color-132 { background-color: #af5f87; }
|
||||
.term-fg-color-132 { color: #af5f87; }
|
||||
.term-bg-color-133 { background-color: #af5faf; }
|
||||
.term-fg-color-133 { color: #af5faf; }
|
||||
.term-bg-color-134 { background-color: #af5fd7; }
|
||||
.term-fg-color-134 { color: #af5fd7; }
|
||||
.term-bg-color-135 { background-color: #af5fff; }
|
||||
.term-fg-color-135 { color: #af5fff; }
|
||||
.term-bg-color-136 { background-color: #af8700; }
|
||||
.term-fg-color-136 { color: #af8700; }
|
||||
.term-bg-color-137 { background-color: #af875f; }
|
||||
.term-fg-color-137 { color: #af875f; }
|
||||
.term-bg-color-138 { background-color: #af8787; }
|
||||
.term-fg-color-138 { color: #af8787; }
|
||||
.term-bg-color-139 { background-color: #af87af; }
|
||||
.term-fg-color-139 { color: #af87af; }
|
||||
.term-bg-color-140 { background-color: #af87d7; }
|
||||
.term-fg-color-140 { color: #af87d7; }
|
||||
.term-bg-color-141 { background-color: #af87ff; }
|
||||
.term-fg-color-141 { color: #af87ff; }
|
||||
.term-bg-color-142 { background-color: #afaf00; }
|
||||
.term-fg-color-142 { color: #afaf00; }
|
||||
.term-bg-color-143 { background-color: #afaf5f; }
|
||||
.term-fg-color-143 { color: #afaf5f; }
|
||||
.term-bg-color-144 { background-color: #afaf87; }
|
||||
.term-fg-color-144 { color: #afaf87; }
|
||||
.term-bg-color-145 { background-color: #afafaf; }
|
||||
.term-fg-color-145 { color: #afafaf; }
|
||||
.term-bg-color-146 { background-color: #afafd7; }
|
||||
.term-fg-color-146 { color: #afafd7; }
|
||||
.term-bg-color-147 { background-color: #afafff; }
|
||||
.term-fg-color-147 { color: #afafff; }
|
||||
.term-bg-color-148 { background-color: #afd700; }
|
||||
.term-fg-color-148 { color: #afd700; }
|
||||
.term-bg-color-149 { background-color: #afd75f; }
|
||||
.term-fg-color-149 { color: #afd75f; }
|
||||
.term-bg-color-150 { background-color: #afd787; }
|
||||
.term-fg-color-150 { color: #afd787; }
|
||||
.term-bg-color-151 { background-color: #afd7af; }
|
||||
.term-fg-color-151 { color: #afd7af; }
|
||||
.term-bg-color-152 { background-color: #afd7d7; }
|
||||
.term-fg-color-152 { color: #afd7d7; }
|
||||
.term-bg-color-153 { background-color: #afd7ff; }
|
||||
.term-fg-color-153 { color: #afd7ff; }
|
||||
.term-bg-color-154 { background-color: #afff00; }
|
||||
.term-fg-color-154 { color: #afff00; }
|
||||
.term-bg-color-155 { background-color: #afff5f; }
|
||||
.term-fg-color-155 { color: #afff5f; }
|
||||
.term-bg-color-156 { background-color: #afff87; }
|
||||
.term-fg-color-156 { color: #afff87; }
|
||||
.term-bg-color-157 { background-color: #afffaf; }
|
||||
.term-fg-color-157 { color: #afffaf; }
|
||||
.term-bg-color-158 { background-color: #afffd7; }
|
||||
.term-fg-color-158 { color: #afffd7; }
|
||||
.term-bg-color-159 { background-color: #afffff; }
|
||||
.term-fg-color-159 { color: #afffff; }
|
||||
.term-bg-color-160 { background-color: #d70000; }
|
||||
.term-fg-color-160 { color: #d70000; }
|
||||
.term-bg-color-161 { background-color: #d7005f; }
|
||||
.term-fg-color-161 { color: #d7005f; }
|
||||
.term-bg-color-162 { background-color: #d70087; }
|
||||
.term-fg-color-162 { color: #d70087; }
|
||||
.term-bg-color-163 { background-color: #d700af; }
|
||||
.term-fg-color-163 { color: #d700af; }
|
||||
.term-bg-color-164 { background-color: #d700d7; }
|
||||
.term-fg-color-164 { color: #d700d7; }
|
||||
.term-bg-color-165 { background-color: #d700ff; }
|
||||
.term-fg-color-165 { color: #d700ff; }
|
||||
.term-bg-color-166 { background-color: #d75f00; }
|
||||
.term-fg-color-166 { color: #d75f00; }
|
||||
.term-bg-color-167 { background-color: #d75f5f; }
|
||||
.term-fg-color-167 { color: #d75f5f; }
|
||||
.term-bg-color-168 { background-color: #d75f87; }
|
||||
.term-fg-color-168 { color: #d75f87; }
|
||||
.term-bg-color-169 { background-color: #d75faf; }
|
||||
.term-fg-color-169 { color: #d75faf; }
|
||||
.term-bg-color-170 { background-color: #d75fd7; }
|
||||
.term-fg-color-170 { color: #d75fd7; }
|
||||
.term-bg-color-171 { background-color: #d75fff; }
|
||||
.term-fg-color-171 { color: #d75fff; }
|
||||
.term-bg-color-172 { background-color: #d78700; }
|
||||
.term-fg-color-172 { color: #d78700; }
|
||||
.term-bg-color-173 { background-color: #d7875f; }
|
||||
.term-fg-color-173 { color: #d7875f; }
|
||||
.term-bg-color-174 { background-color: #d78787; }
|
||||
.term-fg-color-174 { color: #d78787; }
|
||||
.term-bg-color-175 { background-color: #d787af; }
|
||||
.term-fg-color-175 { color: #d787af; }
|
||||
.term-bg-color-176 { background-color: #d787d7; }
|
||||
.term-fg-color-176 { color: #d787d7; }
|
||||
.term-bg-color-177 { background-color: #d787ff; }
|
||||
.term-fg-color-177 { color: #d787ff; }
|
||||
.term-bg-color-178 { background-color: #d7af00; }
|
||||
.term-fg-color-178 { color: #d7af00; }
|
||||
.term-bg-color-179 { background-color: #d7af5f; }
|
||||
.term-fg-color-179 { color: #d7af5f; }
|
||||
.term-bg-color-180 { background-color: #d7af87; }
|
||||
.term-fg-color-180 { color: #d7af87; }
|
||||
.term-bg-color-181 { background-color: #d7afaf; }
|
||||
.term-fg-color-181 { color: #d7afaf; }
|
||||
.term-bg-color-182 { background-color: #d7afd7; }
|
||||
.term-fg-color-182 { color: #d7afd7; }
|
||||
.term-bg-color-183 { background-color: #d7afff; }
|
||||
.term-fg-color-183 { color: #d7afff; }
|
||||
.term-bg-color-184 { background-color: #d7d700; }
|
||||
.term-fg-color-184 { color: #d7d700; }
|
||||
.term-bg-color-185 { background-color: #d7d75f; }
|
||||
.term-fg-color-185 { color: #d7d75f; }
|
||||
.term-bg-color-186 { background-color: #d7d787; }
|
||||
.term-fg-color-186 { color: #d7d787; }
|
||||
.term-bg-color-187 { background-color: #d7d7af; }
|
||||
.term-fg-color-187 { color: #d7d7af; }
|
||||
.term-bg-color-188 { background-color: #d7d7d7; }
|
||||
.term-fg-color-188 { color: #d7d7d7; }
|
||||
.term-bg-color-189 { background-color: #d7d7ff; }
|
||||
.term-fg-color-189 { color: #d7d7ff; }
|
||||
.term-bg-color-190 { background-color: #d7ff00; }
|
||||
.term-fg-color-190 { color: #d7ff00; }
|
||||
.term-bg-color-191 { background-color: #d7ff5f; }
|
||||
.term-fg-color-191 { color: #d7ff5f; }
|
||||
.term-bg-color-192 { background-color: #d7ff87; }
|
||||
.term-fg-color-192 { color: #d7ff87; }
|
||||
.term-bg-color-193 { background-color: #d7ffaf; }
|
||||
.term-fg-color-193 { color: #d7ffaf; }
|
||||
.term-bg-color-194 { background-color: #d7ffd7; }
|
||||
.term-fg-color-194 { color: #d7ffd7; }
|
||||
.term-bg-color-195 { background-color: #d7ffff; }
|
||||
.term-fg-color-195 { color: #d7ffff; }
|
||||
.term-bg-color-196 { background-color: #ff0000; }
|
||||
.term-fg-color-196 { color: #ff0000; }
|
||||
.term-bg-color-197 { background-color: #ff005f; }
|
||||
.term-fg-color-197 { color: #ff005f; }
|
||||
.term-bg-color-198 { background-color: #ff0087; }
|
||||
.term-fg-color-198 { color: #ff0087; }
|
||||
.term-bg-color-199 { background-color: #ff00af; }
|
||||
.term-fg-color-199 { color: #ff00af; }
|
||||
.term-bg-color-200 { background-color: #ff00d7; }
|
||||
.term-fg-color-200 { color: #ff00d7; }
|
||||
.term-bg-color-201 { background-color: #ff00ff; }
|
||||
.term-fg-color-201 { color: #ff00ff; }
|
||||
.term-bg-color-202 { background-color: #ff5f00; }
|
||||
.term-fg-color-202 { color: #ff5f00; }
|
||||
.term-bg-color-203 { background-color: #ff5f5f; }
|
||||
.term-fg-color-203 { color: #ff5f5f; }
|
||||
.term-bg-color-204 { background-color: #ff5f87; }
|
||||
.term-fg-color-204 { color: #ff5f87; }
|
||||
.term-bg-color-205 { background-color: #ff5faf; }
|
||||
.term-fg-color-205 { color: #ff5faf; }
|
||||
.term-bg-color-206 { background-color: #ff5fd7; }
|
||||
.term-fg-color-206 { color: #ff5fd7; }
|
||||
.term-bg-color-207 { background-color: #ff5fff; }
|
||||
.term-fg-color-207 { color: #ff5fff; }
|
||||
.term-bg-color-208 { background-color: #ff8700; }
|
||||
.term-fg-color-208 { color: #ff8700; }
|
||||
.term-bg-color-209 { background-color: #ff875f; }
|
||||
.term-fg-color-209 { color: #ff875f; }
|
||||
.term-bg-color-210 { background-color: #ff8787; }
|
||||
.term-fg-color-210 { color: #ff8787; }
|
||||
.term-bg-color-211 { background-color: #ff87af; }
|
||||
.term-fg-color-211 { color: #ff87af; }
|
||||
.term-bg-color-212 { background-color: #ff87d7; }
|
||||
.term-fg-color-212 { color: #ff87d7; }
|
||||
.term-bg-color-213 { background-color: #ff87ff; }
|
||||
.term-fg-color-213 { color: #ff87ff; }
|
||||
.term-bg-color-214 { background-color: #ffaf00; }
|
||||
.term-fg-color-214 { color: #ffaf00; }
|
||||
.term-bg-color-215 { background-color: #ffaf5f; }
|
||||
.term-fg-color-215 { color: #ffaf5f; }
|
||||
.term-bg-color-216 { background-color: #ffaf87; }
|
||||
.term-fg-color-216 { color: #ffaf87; }
|
||||
.term-bg-color-217 { background-color: #ffafaf; }
|
||||
.term-fg-color-217 { color: #ffafaf; }
|
||||
.term-bg-color-218 { background-color: #ffafd7; }
|
||||
.term-fg-color-218 { color: #ffafd7; }
|
||||
.term-bg-color-219 { background-color: #ffafff; }
|
||||
.term-fg-color-219 { color: #ffafff; }
|
||||
.term-bg-color-220 { background-color: #ffd700; }
|
||||
.term-fg-color-220 { color: #ffd700; }
|
||||
.term-bg-color-221 { background-color: #ffd75f; }
|
||||
.term-fg-color-221 { color: #ffd75f; }
|
||||
.term-bg-color-222 { background-color: #ffd787; }
|
||||
.term-fg-color-222 { color: #ffd787; }
|
||||
.term-bg-color-223 { background-color: #ffd7af; }
|
||||
.term-fg-color-223 { color: #ffd7af; }
|
||||
.term-bg-color-224 { background-color: #ffd7d7; }
|
||||
.term-fg-color-224 { color: #ffd7d7; }
|
||||
.term-bg-color-225 { background-color: #ffd7ff; }
|
||||
.term-fg-color-225 { color: #ffd7ff; }
|
||||
.term-bg-color-226 { background-color: #ffff00; }
|
||||
.term-fg-color-226 { color: #ffff00; }
|
||||
.term-bg-color-227 { background-color: #ffff5f; }
|
||||
.term-fg-color-227 { color: #ffff5f; }
|
||||
.term-bg-color-228 { background-color: #ffff87; }
|
||||
.term-fg-color-228 { color: #ffff87; }
|
||||
.term-bg-color-229 { background-color: #ffffaf; }
|
||||
.term-fg-color-229 { color: #ffffaf; }
|
||||
.term-bg-color-230 { background-color: #ffffd7; }
|
||||
.term-fg-color-230 { color: #ffffd7; }
|
||||
.term-bg-color-231 { background-color: #ffffff; }
|
||||
.term-fg-color-231 { color: #ffffff; }
|
||||
.term-bg-color-232 { background-color: #080808; }
|
||||
.term-fg-color-232 { color: #080808; }
|
||||
.term-bg-color-233 { background-color: #121212; }
|
||||
.term-fg-color-233 { color: #121212; }
|
||||
.term-bg-color-234 { background-color: #1c1c1c; }
|
||||
.term-fg-color-234 { color: #1c1c1c; }
|
||||
.term-bg-color-235 { background-color: #262626; }
|
||||
.term-fg-color-235 { color: #262626; }
|
||||
.term-bg-color-236 { background-color: #303030; }
|
||||
.term-fg-color-236 { color: #303030; }
|
||||
.term-bg-color-237 { background-color: #3a3a3a; }
|
||||
.term-fg-color-237 { color: #3a3a3a; }
|
||||
.term-bg-color-238 { background-color: #444444; }
|
||||
.term-fg-color-238 { color: #444444; }
|
||||
.term-bg-color-239 { background-color: #4e4e4e; }
|
||||
.term-fg-color-239 { color: #4e4e4e; }
|
||||
.term-bg-color-240 { background-color: #585858; }
|
||||
.term-fg-color-240 { color: #585858; }
|
||||
.term-bg-color-241 { background-color: #626262; }
|
||||
.term-fg-color-241 { color: #626262; }
|
||||
.term-bg-color-242 { background-color: #6c6c6c; }
|
||||
.term-fg-color-242 { color: #6c6c6c; }
|
||||
.term-bg-color-243 { background-color: #767676; }
|
||||
.term-fg-color-243 { color: #767676; }
|
||||
.term-bg-color-244 { background-color: #808080; }
|
||||
.term-fg-color-244 { color: #808080; }
|
||||
.term-bg-color-245 { background-color: #8a8a8a; }
|
||||
.term-fg-color-245 { color: #8a8a8a; }
|
||||
.term-bg-color-246 { background-color: #949494; }
|
||||
.term-fg-color-246 { color: #949494; }
|
||||
.term-bg-color-247 { background-color: #9e9e9e; }
|
||||
.term-fg-color-247 { color: #9e9e9e; }
|
||||
.term-bg-color-248 { background-color: #a8a8a8; }
|
||||
.term-fg-color-248 { color: #a8a8a8; }
|
||||
.term-bg-color-249 { background-color: #b2b2b2; }
|
||||
.term-fg-color-249 { color: #b2b2b2; }
|
||||
.term-bg-color-250 { background-color: #bcbcbc; }
|
||||
.term-fg-color-250 { color: #bcbcbc; }
|
||||
.term-bg-color-251 { background-color: #c6c6c6; }
|
||||
.term-fg-color-251 { color: #c6c6c6; }
|
||||
.term-bg-color-252 { background-color: #d0d0d0; }
|
||||
.term-fg-color-252 { color: #d0d0d0; }
|
||||
.term-bg-color-253 { background-color: #dadada; }
|
||||
.term-fg-color-253 { color: #dadada; }
|
||||
.term-bg-color-254 { background-color: #e4e4e4; }
|
||||
.term-fg-color-254 { color: #e4e4e4; }
|
||||
.term-bg-color-255 { background-color: #eeeeee; }
|
||||
.term-fg-color-255 { color: #eeeeee; }
|
||||
.term-bg-color-default { background-color: #000000; }
|
||||
.term-bg-color-256 { background-color: #000000; }
|
||||
.term-fg-color-256 { color: #000000; }
|
||||
.term-fg-color-default { color: #f0f0f0; }
|
||||
.term-bg-color-257 { background-color: #f0f0f0; }
|
||||
.term-fg-color-257 { color: #f0f0f0; }
|
||||
.term-bold { font-weight: bold; }
|
||||
.term-underline { text-decoration: underline; }
|
||||
.term-blink { text-decoration: blink; }
|
||||
.term-hidden { visibility: hidden; }
|
||||
22
src/pkg/lib/variables.less
Normal file
22
src/pkg/lib/variables.less
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
@import (less) "../../../node_modules/bootstrap/less/variables.less";
|
||||
@import (less) "../../../node_modules/patternfly/dist/less/variables.less";
|
||||
|
||||
@metadata-color: #888;
|
||||
|
||||
@listing-ct-hover: @list-group-hover-bg;
|
||||
@listing-ct-active: #65bedf;
|
||||
@listing-ct-padding: 10px;
|
||||
@listing-ct-spacing: 15px;
|
||||
@listing-ct-open: #f5f5f5;
|
||||
@listing-ct-open-width: 3px;
|
||||
@listing-ct-metadata: @metadata-color;
|
||||
@listing-ct-warning-color: #fbc7c7;
|
||||
@listing-ct-border: #ccc;
|
||||
@listing-ct-border-light: #eee;
|
||||
@listing-ct-border-maybe: #ddd;
|
||||
|
||||
@screen-lg-max: (@screen-xlg-min - 1);
|
||||
@screen-xlg-min: 1600px;
|
||||
@screen-xs: 480px;
|
||||
@screen-xs-min: @screen-xs;
|
||||
@screen-xxs-max: (@screen-xs-min - 1);
|
||||
1080
src/player.jsx
Normal file
1080
src/player.jsx
Normal file
File diff suppressed because it is too large
Load diff
368
src/recordings.css
Normal file
368
src/recordings.css
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
@import "/page.css";
|
||||
|
||||
@import "/console.css";
|
||||
@import "/journal.css";
|
||||
@import "/plot.css";
|
||||
@import "/table.css";
|
||||
|
||||
@import "./timer.css";
|
||||
|
||||
a.disabled {
|
||||
text-decoration: none;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.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-address-spinner {
|
||||
margin-left: -30px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.realms-op-address-error {
|
||||
margin-left: -30px;
|
||||
margin-top: 2px;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.realms-op-zero-width {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.realms-op-error {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
overflow: auto;
|
||||
max-width: 550px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
/* Other styles */
|
||||
|
||||
.fa-red {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.small-messages {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#server-graph-toolbar .dropdown {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#server-graph-toolbar .dropdown-toggle span {
|
||||
width: 6em;
|
||||
text-align: left;
|
||||
padding-left: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.server-graph {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.server-graph-legend {
|
||||
list-style-type: none;
|
||||
padding: 30px 40px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.server-graph-legend i {
|
||||
padding-right: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.server-graph-legend .cpu-io-wait i {
|
||||
color: #e41a1c;
|
||||
}
|
||||
|
||||
.server-graph-legend .cpu-kernel i {
|
||||
color: #ff7f00;
|
||||
}
|
||||
|
||||
.server-graph-legend .cpu-user i {
|
||||
color: #377eb8;
|
||||
}
|
||||
|
||||
.server-graph-legend .cpu-nice i {
|
||||
color: #4daf4a;
|
||||
}
|
||||
|
||||
.server-graph-legend .memory-swap i {
|
||||
color: #e41a1c;
|
||||
}
|
||||
|
||||
.server-graph-legend .memory-cached i {
|
||||
color: #ff7f00;
|
||||
}
|
||||
|
||||
.server-graph-legend .memory-used i {
|
||||
color: #377eb8;
|
||||
}
|
||||
|
||||
.server-graph-legend .memory-free i {
|
||||
color: #4daf4a;
|
||||
}
|
||||
|
||||
#cpu_status_graph,
|
||||
#memory_status_graph {
|
||||
height: 400px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#sich-note-1,
|
||||
#sich-note-2 {
|
||||
margin: 0;
|
||||
}
|
||||
#shutdown-dialog td {
|
||||
padding-right: 20px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#shutdown-dialog .opt {
|
||||
padding: 1px 10px;
|
||||
}
|
||||
|
||||
#shutdown-dialog .dropdown {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#shutdown-group {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#shutdown-dialog textarea {
|
||||
resize: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#shutdown-dialog input {
|
||||
display: inline;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
#shutdown-dialog .shutdown-hours,
|
||||
#shutdown-dialog .shutdown-minutes {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
#system_information_ssh_keys .list-group-item {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
#system_information_hardware_text,
|
||||
#system_information_os_text {
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.cockpit-modal-md {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Make sure to not break log message lines in order to preserve information */
|
||||
#journal-entry .info-table-ct td {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.service-unit-description {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.service-unit-data {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.service-unit-failed {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.service-action-btn ul {
|
||||
right: 0px;
|
||||
left: auto;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.service-panel td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.service-panel span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.service-panel td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.service-panel table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#services > .container-fluid {
|
||||
margin-top: 5em;
|
||||
}
|
||||
|
||||
.service-template input {
|
||||
width: 50em;
|
||||
}
|
||||
|
||||
#journal-current-day-menu dropdown-toggle {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#journal-box {
|
||||
margin-top: 5em;
|
||||
}
|
||||
|
||||
#journal-entry-message {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#journal-entry-fields {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Extra content header */
|
||||
|
||||
.content-header-extra {
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
z-index: 900;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.content-header-extra .btn-group:not(:first-child) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#motd {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.listing > tbody > tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.margin-right-btn {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
min-width: 34px;
|
||||
}
|
||||
|
||||
.sort {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sort span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
width:7px;
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
table.listing-ct > thead th:last-child, tr.listing-ct-item td:last-child {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.date {
|
||||
width: 185px;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
background-color: #ffdddd;
|
||||
}
|
||||
|
||||
.valid {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.player-wrap {
|
||||
min-width: 672px;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.player-wrap .panel-body, .player-wrap .console-ct > .terminal {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dragnpan {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
748
src/recordings.jsx
Normal file
748
src/recordings.jsx
Normal file
|
|
@ -0,0 +1,748 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2017 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/>.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
let $ = require("jquery");
|
||||
let cockpit = require("cockpit");
|
||||
let _ = cockpit.gettext;
|
||||
let moment = require("moment");
|
||||
let Journal = require("journal");
|
||||
let React = require("react");
|
||||
let Listing = require("cockpit-components-listing.jsx");
|
||||
let Player = require("./player.jsx");
|
||||
|
||||
require("bootstrap-datetime-picker/js/bootstrap-datetimepicker.js");
|
||||
require("bootstrap-datetime-picker/css/bootstrap-datetimepicker.css");
|
||||
|
||||
/*
|
||||
* Convert a number to integer number string and pad with zeroes to
|
||||
* specified width.
|
||||
*/
|
||||
let padInt = function (n, w) {
|
||||
let i = Math.floor(n);
|
||||
let a = Math.abs(i);
|
||||
let s = a.toString();
|
||||
for (w -= s.length; w > 0; w--) {
|
||||
s = '0' + s;
|
||||
}
|
||||
return ((i < 0) ? '-' : '') + s;
|
||||
}
|
||||
|
||||
/*
|
||||
* Format date and time for a number of milliseconds since Epoch.
|
||||
*/
|
||||
let formatDateTime = function (ms) {
|
||||
let d = new Date(ms);
|
||||
return (
|
||||
padInt(d.getFullYear(), 4) + '-' +
|
||||
padInt(d.getMonth() + 1, 2) + '-' +
|
||||
padInt(d.getDate(), 2) + ' ' +
|
||||
padInt(d.getHours(), 2) + ':' +
|
||||
padInt(d.getMinutes(), 2) + ':' +
|
||||
padInt(d.getSeconds(), 2)
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* Format a time interval from a number of milliseconds.
|
||||
*/
|
||||
let formatDuration = function (ms) {
|
||||
let v = Math.floor(ms / 1000);
|
||||
let s = Math.floor(v % 60);
|
||||
v = Math.floor(v / 60);
|
||||
let m = Math.floor(v % 60);
|
||||
v = Math.floor(v / 60);
|
||||
let h = Math.floor(v % 24);
|
||||
let d = Math.floor(v / 24);
|
||||
let str = '';
|
||||
|
||||
if (d > 0) {
|
||||
str += d + ' ' + _("days") + ' ';
|
||||
}
|
||||
|
||||
if (h > 0 || str.length > 0) {
|
||||
str += padInt(h, 2) + ':';
|
||||
}
|
||||
|
||||
str += padInt(m, 2) + ':' + padInt(s, 2);
|
||||
|
||||
return (ms < 0 ? '-' : '') + str;
|
||||
};
|
||||
|
||||
let parseDate = function(date) {
|
||||
let regex = new RegExp(/^\s*(\d\d\d\d-\d\d-\d\d)(\s+(\d\d:\d\d(:\d\d)?))?\s*$/);
|
||||
|
||||
let captures = regex.exec(date);
|
||||
|
||||
if (captures != null) {
|
||||
let date = captures[1];
|
||||
if (captures[3]) {
|
||||
date = date + " " + captures[3];
|
||||
}
|
||||
if (moment(date, ["YYYY-M-D H:m:s", "YYYY-M-D H:m", "YYYY-M-D"], true).isValid()) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
if (date === "" || date === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* A component representing a date & time picker based on bootstrap-datetime-picker.
|
||||
* Requires jQuery, bootstrap-datetime-picker, moment.js
|
||||
* Properties:
|
||||
* - onDateChange: function to call on date change event of datepicker.
|
||||
* - date: variable to pass which will be used as initial value.
|
||||
*/
|
||||
let Datetimepicker = class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleDateChange = this.handleDateChange.bind(this);
|
||||
this.clearField = this.clearField.bind(this);
|
||||
this.markDateField = this.markDateField.bind(this);
|
||||
this.state = {
|
||||
invalid: false,
|
||||
date: this.props.date,
|
||||
dateLastValid: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let funcDate = this.handleDateChange;
|
||||
let datepicker = $(this.refs.datepicker).datetimepicker({
|
||||
format: 'yyyy-mm-dd hh:ii:00',
|
||||
autoclose: true,
|
||||
todayBtn: true,
|
||||
});
|
||||
datepicker.on('changeDate', function(e) {
|
||||
funcDate(e);
|
||||
});
|
||||
$(this.refs.datepicker_input).datetimepicker('remove');
|
||||
this.markDateField();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
$(this.textInput).datetimepicker('remove');
|
||||
}
|
||||
|
||||
handleDateChange(e) {
|
||||
if (e.type === "changeDate") {
|
||||
let event = new Event('input', { bubbles: true });
|
||||
e.currentTarget.firstChild.dispatchEvent(event);
|
||||
}
|
||||
|
||||
if (e.type === "input") {
|
||||
this.setState({date: e.target.value});
|
||||
if (parseDate(e.target.value)) {
|
||||
this.setState({dateLastValid: e.target.value});
|
||||
this.setState({invalid: false});
|
||||
this.props.onDateChange(e.target.value, e.target.value.trim());
|
||||
} else {
|
||||
this.setState({invalid: true});
|
||||
this.props.onDateChange(e.target.value, this.state.dateLastValid.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearField() {
|
||||
$(this.refs.datepicker_input).val("");
|
||||
let event = new Event('input', { bubbles: true });
|
||||
this.refs.datepicker_input.dispatchEvent(event);
|
||||
this.handleDateChange(event);
|
||||
this.setState({invalid: false});
|
||||
}
|
||||
|
||||
markDateField() {
|
||||
let date = $(this.refs.datepicker_input).val()
|
||||
.trim();
|
||||
if (!parseDate(date)) {
|
||||
this.setState({invalid: true});
|
||||
} else {
|
||||
this.setState({dateLastValid: date});
|
||||
this.setState({invalid: false});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref="datepicker" className="input-group date input-append date form_datetime">
|
||||
<input ref="datepicker_input" type="text" size="16"
|
||||
className={"form-control bootstrap-datepicker " + (this.state.invalid ? "invalid" : "valid")}
|
||||
readOnly value={this.state.date} onChange={this.handleDateChange} />
|
||||
<span className="input-group-addon add-on"><i className="fa fa-calendar" /></span>
|
||||
<span className="input-group-addon add-on" onClick={this.clearField}>
|
||||
<i className="fa fa-remove" /></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A component representing a username input text field.
|
||||
* TODO make as a select / drop-down with list of exisiting users.
|
||||
*/
|
||||
let UserPicker = class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleUsernameChange = this.handleUsernameChange.bind(this);
|
||||
}
|
||||
|
||||
handleUsernameChange(e) {
|
||||
this.props.onUsernameChange(e.target.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="input-group">
|
||||
<input type="text" className="form-control" value={this.props.username}
|
||||
onChange={this.handleUsernameChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A component representing a single recording view.
|
||||
* Properties:
|
||||
* - recording: either null for no recording data available yet, or a
|
||||
* recording object, as created by the View below.
|
||||
*/
|
||||
let Recording = class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.goBackToList = this.goBackToList.bind(this);
|
||||
}
|
||||
|
||||
goBackToList() {
|
||||
if (cockpit.location.path[0]) {
|
||||
cockpit.location.go([], cockpit.location.options);
|
||||
} else {
|
||||
cockpit.location.go('/');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let r = this.props.recording;
|
||||
if (r == null) {
|
||||
return <span>Loading...</span>;
|
||||
} else {
|
||||
let player =
|
||||
(<Player.Player
|
||||
ref="player"
|
||||
matchList={this.props.recording.matchList} />);
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<ol className="breadcrumb">
|
||||
<li><a onClick={this.goBackToList}>Session Recording</a></li>
|
||||
<li className="active">Session</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">
|
||||
<span>{_("Recording")}</span>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<table className="form-table-ct">
|
||||
<tr>
|
||||
<td>{_("ID")}</td>
|
||||
<td>{r.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_("Boot ID")}</td>
|
||||
<td>{r.boot_id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_("Session ID")}</td>
|
||||
<td>{r.session_id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_("PID")}</td>
|
||||
<td>{r.pid}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_("Start")}</td>
|
||||
<td>{formatDateTime(r.start)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_("End")}</td>
|
||||
<td>{formatDateTime(r.end)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_("Duration")}</td>
|
||||
<td>{formatDuration(r.end - r.start)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_("User")}</td>
|
||||
<td>{r.user}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6 player-wrap">
|
||||
{player}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* A component representing a list of recordings.
|
||||
* Properties:
|
||||
* - list: an array with recording objects, as created by the View below
|
||||
*/
|
||||
let RecordingList = class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleColumnClick = this.handleColumnClick.bind(this);
|
||||
this.getSortedList = this.getSortedList.bind(this);
|
||||
this.drawSortDir = this.drawSortDir.bind(this);
|
||||
this.state = {
|
||||
sorting_field: "start",
|
||||
sorting_asc: true,
|
||||
};
|
||||
}
|
||||
|
||||
drawSortDir() {
|
||||
$('#sort_arrow').remove();
|
||||
let type = this.state.sorting_asc ? "asc" : "desc";
|
||||
let arrow = '<i id="sort_arrow" class="fa fa-sort-' + type + '" aria-hidden="true"></i>';
|
||||
$(this.refs[this.state.sorting_field]).append(arrow);
|
||||
}
|
||||
|
||||
handleColumnClick(event) {
|
||||
if (this.state.sorting_field === event.currentTarget.id) {
|
||||
this.setState({sorting_asc: !this.state.sorting_asc});
|
||||
} else {
|
||||
this.setState({
|
||||
sorting_field: event.currentTarget.id,
|
||||
sorting_asc: 'asc'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSortedList() {
|
||||
let field = this.state.sorting_field;
|
||||
let asc = this.state.sorting_asc;
|
||||
let list = this.props.list.slice();
|
||||
|
||||
if (this.state.sorting_field != null) {
|
||||
if (asc) {
|
||||
list.sort(function(a, b) {
|
||||
return a[field] > b[field];
|
||||
});
|
||||
} else {
|
||||
list.sort(function(a, b) {
|
||||
return a[field] < b[field];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the cockpit location to point to the specified recording.
|
||||
*/
|
||||
navigateToRecording(recording) {
|
||||
cockpit.location.go([recording.id], cockpit.location.options);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.drawSortDir();
|
||||
}
|
||||
|
||||
render() {
|
||||
let columnTitles = [
|
||||
(<div id="user" className="sort" onClick={this.handleColumnClick}><span>{_("User")}</span> <div
|
||||
ref="user" className="sort-icon" /></div>),
|
||||
(<div id="start" className="sort" onClick={this.handleColumnClick}><span>{_("Start")}</span> <div
|
||||
ref="start" className="sort-icon" /></div>),
|
||||
(<div id="end" className="sort" onClick={this.handleColumnClick}><span>{_("End")}</span> <div
|
||||
ref="end" className="sort-icon" /></div>),
|
||||
(<div id="duration" className="sort" onClick={this.handleColumnClick}><span>{_("Duration")}</span> <div
|
||||
ref="duration" className="sort-icon" /></div>),
|
||||
];
|
||||
let list = this.getSortedList();
|
||||
let rows = [];
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let r = list[i];
|
||||
let columns = [r.user,
|
||||
formatDateTime(r.start),
|
||||
formatDateTime(r.end),
|
||||
formatDuration(r.end - r.start)];
|
||||
rows.push(<Listing.ListingRow
|
||||
rowId={r.id}
|
||||
columns={columns}
|
||||
navigateToItem={this.navigateToRecording.bind(this, r)} />);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="content-header-extra">
|
||||
<table className="form-table-ct">
|
||||
<th>
|
||||
<td className="top">
|
||||
<label className="control-label" htmlFor="dateSince">Since</label>
|
||||
</td>
|
||||
<td>
|
||||
<Datetimepicker onDateChange={this.props.onDateSinceChange}
|
||||
date={this.props.dateSince} />
|
||||
</td>
|
||||
<td className="top">
|
||||
<label className="control-label" htmlFor="dateUntil">Until</label>
|
||||
</td>
|
||||
<td>
|
||||
<Datetimepicker onDateChange={this.props.onDateUntilChange}
|
||||
date={this.props.dateUntil} />
|
||||
</td>
|
||||
<td className="top">
|
||||
<label className="control-label" htmlFor="username">Username</label>
|
||||
</td>
|
||||
<td>
|
||||
<UserPicker onUsernameChange={this.props.onUsernameChange}
|
||||
username={this.props.username} />
|
||||
</td>
|
||||
<td className="top">
|
||||
<label className="control-label" htmlFor="config">Configuration</label>
|
||||
</td>
|
||||
<td className="top">
|
||||
<a href="/cockpit/@localhost/session_recording/config.html" className="btn btn-default" data-toggle="modal">
|
||||
<i className="fa fa-cog" aria-hidden="true" /></a>
|
||||
</td>
|
||||
</th>
|
||||
</table>
|
||||
</div>
|
||||
<Listing.Listing title={_("Sessions")}
|
||||
columnTitles={columnTitles}
|
||||
emptyCaption={_("No recorded sessions")}
|
||||
fullWidth={false}>
|
||||
{rows}
|
||||
</Listing.Listing>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* A component representing the view upon a list of recordings, or a
|
||||
* single recording. Extracts the ID of the recording to display from
|
||||
* cockpit.location.path[0]. If it's zero, displays the list.
|
||||
*/
|
||||
let View = class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onLocationChanged = this.onLocationChanged.bind(this);
|
||||
this.journalctlIngest = this.journalctlIngest.bind(this);
|
||||
this.handleDateSinceChange = this.handleDateSinceChange.bind(this);
|
||||
this.handleDateUntilChange = this.handleDateUntilChange.bind(this);
|
||||
this.handleUsernameChange = this.handleUsernameChange.bind(this);
|
||||
/* Journalctl instance */
|
||||
this.journalctl = null;
|
||||
/* Recording ID journalctl instance is invoked with */
|
||||
this.journalctlRecordingID = null;
|
||||
/* Recording ID -> data map */
|
||||
this.recordingMap = {};
|
||||
/* tlog UID in system set in ComponentDidMount */
|
||||
this.uid = null;
|
||||
this.state = {
|
||||
/* List of recordings in start order */
|
||||
recordingList: [],
|
||||
/* ID of the recording to display, or null for all */
|
||||
recordingID: cockpit.location.path[0] || null,
|
||||
dateSince: cockpit.location.options.dateSince || null,
|
||||
dateSinceLastValid: null,
|
||||
dateUntil: cockpit.location.options.dateUntil || null,
|
||||
dateUntilLastValid: null,
|
||||
/* value to filter recordings by username */
|
||||
username: cockpit.location.options.username || null,
|
||||
error_tlog_uid: false,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Display a journalctl error
|
||||
*/
|
||||
journalctlError(error) {
|
||||
console.warn(cockpit.message(error));
|
||||
}
|
||||
|
||||
/*
|
||||
* Respond to cockpit location change by extracting and setting the
|
||||
* displayed recording ID.
|
||||
*/
|
||||
onLocationChanged() {
|
||||
this.setState({
|
||||
recordingID: cockpit.location.path[0] || null,
|
||||
dateSince: cockpit.location.options.dateSince || null,
|
||||
dateUntil: cockpit.location.options.dateUntil || null,
|
||||
username: cockpit.location.options.username || null,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Ingest journal entries sent by journalctl.
|
||||
*/
|
||||
journalctlIngest(entryList) {
|
||||
let recordingList = this.state.recordingList.slice();
|
||||
let i;
|
||||
let j;
|
||||
|
||||
for (i = 0; i < entryList.length; i++) {
|
||||
let e = entryList[i];
|
||||
let id = e['TLOG_REC'];
|
||||
|
||||
/* Skip entries with missing recording ID */
|
||||
if (id === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ts = Math.floor(
|
||||
parseInt(e["__REALTIME_TIMESTAMP"], 10) /
|
||||
1000);
|
||||
|
||||
let r = this.recordingMap[id];
|
||||
/* If no recording found */
|
||||
if (r === undefined) {
|
||||
/* Create new recording */
|
||||
r = {id: id,
|
||||
matchList: ["_UID=" + this.uid,
|
||||
"TLOG_REC=" + id],
|
||||
user: e["TLOG_USER"],
|
||||
boot_id: e["_BOOT_ID"],
|
||||
session_id: parseInt(e["TLOG_SESSION"], 10),
|
||||
pid: parseInt(e["_PID"], 10),
|
||||
start: ts,
|
||||
/* FIXME Should be start + message duration */
|
||||
end: ts,
|
||||
duration: 0};
|
||||
/* Map the recording */
|
||||
this.recordingMap[id] = r;
|
||||
/* Insert the recording in order */
|
||||
for (j = recordingList.length - 1;
|
||||
j >= 0 && r.start < recordingList[j].start;
|
||||
j--);
|
||||
recordingList.splice(j + 1, 0, r);
|
||||
} else {
|
||||
/* Adjust existing recording */
|
||||
if (ts > r.end) {
|
||||
r.end = ts;
|
||||
r.duration = r.end - r.start;
|
||||
}
|
||||
if (ts < r.start) {
|
||||
r.start = ts;
|
||||
r.duration = r.end - r.start;
|
||||
/* Find the recording in the list */
|
||||
for (j = recordingList.length - 1;
|
||||
j >= 0 && recordingList[j] != r;
|
||||
j--);
|
||||
/* If found */
|
||||
if (j >= 0) {
|
||||
/* Remove */
|
||||
recordingList.splice(j, 1);
|
||||
}
|
||||
/* Insert the recording in order */
|
||||
for (j = recordingList.length - 1;
|
||||
j >= 0 && r.start < recordingList[j].start;
|
||||
j--);
|
||||
recordingList.splice(j + 1, 0, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({recordingList: recordingList});
|
||||
}
|
||||
|
||||
/*
|
||||
* Start journalctl, retrieving entries for the current recording ID.
|
||||
* Assumes journalctl is not running.
|
||||
*/
|
||||
journalctlStart() {
|
||||
let matches = ["_UID=" + this.uid];
|
||||
if (this.state.username) {
|
||||
matches.push("TLOG_USER=" + this.state.username);
|
||||
}
|
||||
|
||||
let options = {follow: true, count: "all"};
|
||||
|
||||
if (this.state.dateSinceLastValid) {
|
||||
options['since'] = this.state.dateSinceLastValid;
|
||||
}
|
||||
|
||||
if (this.state.dateUntil) {
|
||||
options['until'] = this.state.dateUntilLastValid;
|
||||
}
|
||||
|
||||
if (this.state.recordingID !== null) {
|
||||
matches.push("TLOG_REC=" + this.state.recordingID);
|
||||
}
|
||||
|
||||
this.journalctlRecordingID = this.state.recordingID;
|
||||
this.journalctl = Journal.journalctl(matches, options)
|
||||
.fail(this.journalctlError)
|
||||
.stream(this.journalctlIngest);
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if journalctl is running.
|
||||
*/
|
||||
journalctlIsRunning() {
|
||||
return this.journalctl != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Stop current journalctl.
|
||||
* Assumes journalctl is running.
|
||||
*/
|
||||
journalctlStop() {
|
||||
this.journalctl.stop();
|
||||
this.journalctl = null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Restarts journalctl.
|
||||
* Will stop journalctl if it's running.
|
||||
*/
|
||||
journalctlRestart() {
|
||||
if (this.journalctlIsRunning()) {
|
||||
this.journalctl.stop();
|
||||
}
|
||||
this.journalctlStart();
|
||||
}
|
||||
|
||||
/*
|
||||
* Clears previous recordings list.
|
||||
* Will clear service obj recordingMap and state.
|
||||
*/
|
||||
clearRecordings() {
|
||||
this.recordingMap = {};
|
||||
this.setState({recordingList: []});
|
||||
}
|
||||
|
||||
handleDateSinceChange(date, last_valid) {
|
||||
this.setState({dateSinceLastValid: last_valid});
|
||||
cockpit.location.go([], $.extend(cockpit.location.options, { dateSince: date }));
|
||||
}
|
||||
|
||||
handleDateUntilChange(date, last_valid) {
|
||||
this.setState({dateUntilLastValid: last_valid});
|
||||
cockpit.location.go([], $.extend(cockpit.location.options, { dateUntil: date }));
|
||||
}
|
||||
|
||||
handleUsernameChange(username) {
|
||||
cockpit.location.go([], $.extend(cockpit.location.options, { username: username }));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let proc = cockpit.spawn(["getent", "passwd", "tlog"]);
|
||||
|
||||
proc.stream((data) => {
|
||||
this.uid = data.split(":", 3)[2];
|
||||
this.journalctlStart();
|
||||
proc.close();
|
||||
});
|
||||
|
||||
proc.fail(() => {
|
||||
this.setState({error_tlog_uid: true});
|
||||
});
|
||||
|
||||
let dateSince = parseDate(this.state.dateSince);
|
||||
|
||||
if (dateSince && dateSince != true) {
|
||||
this.setState({dateSinceLastValid: dateSince});
|
||||
}
|
||||
|
||||
let dateUntil = parseDate(this.state.dateUntil);
|
||||
|
||||
if (dateUntil && dateUntil != true) {
|
||||
this.setState({dateUntilLastValid: dateUntil});
|
||||
}
|
||||
|
||||
cockpit.addEventListener("locationchanged",
|
||||
this.onLocationChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.journalctlIsRunning()) {
|
||||
this.journalctlStop();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
/*
|
||||
* If we're running a specific (non-wildcard) journalctl
|
||||
* and recording ID has changed
|
||||
*/
|
||||
if (this.journalctlRecordingID !== null &&
|
||||
this.state.recordingID != prevState.recordingID) {
|
||||
if (this.journalctlIsRunning()) {
|
||||
this.journalctlStop();
|
||||
}
|
||||
this.journalctlStart();
|
||||
}
|
||||
if (this.state.dateSinceLastValid != prevState.dateSinceLastValid ||
|
||||
this.state.dateUntilLastValid != prevState.dateUntilLastValid ||
|
||||
this.state.username != prevState.username
|
||||
) {
|
||||
this.clearRecordings();
|
||||
this.journalctlRestart();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error_tlog_uid === true) {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
Error getting tlog UID from system.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (this.state.recordingID === null) {
|
||||
return (
|
||||
<RecordingList
|
||||
onDateSinceChange={this.handleDateSinceChange} dateSince={this.state.dateSince}
|
||||
onDateUntilChange={this.handleDateUntilChange} dateUntil={this.state.dateUntil}
|
||||
onUsernameChange={this.handleUsernameChange} username={this.state.username}
|
||||
list={this.state.recordingList} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Recording recording={this.recordingMap[this.state.recordingID]} />
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
React.render(<View />, document.getElementById('view'));
|
||||
}());
|
||||
191
src/terminal.jsx
Normal file
191
src/terminal.jsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2016 Red Hat, Inc.
|
||||
*
|
||||
* Cockpit is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation; either version 2.1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Cockpit is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
var React = require("react");
|
||||
var Term = require("term");
|
||||
let $ = require("jquery");
|
||||
|
||||
require("console.css");
|
||||
require("jquery-resizable");
|
||||
require("jquery-resizable/resizable.css");
|
||||
|
||||
/*
|
||||
* A terminal component that communicates over a cockpit channel.
|
||||
*
|
||||
* The only required property is 'channel', which must point to a cockpit
|
||||
* stream channel.
|
||||
*
|
||||
* The size of the terminal can be set with the 'rows' and 'cols'
|
||||
* properties. If those properties are not given, the terminal will fill
|
||||
* its container.
|
||||
*
|
||||
* If the 'onTitleChanged' callback property is set, it will be called whenever
|
||||
* the title of the terminal changes.
|
||||
*
|
||||
* Call focus() to set the input focus on the terminal.
|
||||
*/
|
||||
var Terminal = React.createClass({
|
||||
propTypes: {
|
||||
cols: React.PropTypes.number,
|
||||
rows: React.PropTypes.number,
|
||||
channel: React.PropTypes.object.isRequired,
|
||||
onTitleChanged: React.PropTypes.func
|
||||
},
|
||||
|
||||
componentWillMount: function () {
|
||||
var term = new Term({
|
||||
cols: this.state.cols || 80,
|
||||
rows: this.state.rows || 25,
|
||||
screenKeys: true,
|
||||
useStyle: true
|
||||
});
|
||||
|
||||
term.on('data', function(data) {
|
||||
if (this.props.channel.valid)
|
||||
this.props.channel.send(data);
|
||||
}.bind(this));
|
||||
|
||||
if (this.props.onTitleChanged)
|
||||
term.on('title', this.props.onTitleChanged);
|
||||
|
||||
this.setState({ terminal: term });
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
this.state.terminal.open(this.refs.terminal);
|
||||
this.connectChannel();
|
||||
|
||||
let term = this.refs.terminal;
|
||||
let onWindowResize = this.onWindowResize;
|
||||
|
||||
$(function() {
|
||||
$(term).resizable({
|
||||
direction: ['right', 'bottom'],
|
||||
stop: function() {
|
||||
onWindowResize();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.props.rows) {
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
this.onWindowResize();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUpdate: function (nextProps, nextState) {
|
||||
if (nextState.cols !== this.state.cols || nextState.rows !== this.state.rows) {
|
||||
this.state.terminal.resize(nextState.cols, nextState.rows);
|
||||
this.props.channel.control({
|
||||
window: {
|
||||
rows: nextState.rows,
|
||||
cols: nextState.cols
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextProps.channel !== this.props.channel) {
|
||||
this.state.terminal.reset();
|
||||
this.disconnectChannel();
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function (prevProps) {
|
||||
if (prevProps.channel !== this.props.channel)
|
||||
this.connectChannel();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
let style = {
|
||||
'min-width': '300px',
|
||||
'min-height': '100px',
|
||||
}
|
||||
// ensure react never reuses this div by keying it with the terminal widget
|
||||
return <div ref="terminal" style={style} className="console-ct" key={this.state.terminal} />;
|
||||
},
|
||||
|
||||
componentWillUnmount: function () {
|
||||
this.disconnectChannel();
|
||||
this.state.terminal.destroy();
|
||||
},
|
||||
|
||||
onChannelMessage: function (event, data) {
|
||||
if (this.state.terminal) {
|
||||
this.state.terminal.write(data);
|
||||
}
|
||||
},
|
||||
|
||||
onChannelClose: function (event, options) {
|
||||
var term = this.state.terminal;
|
||||
term.write('\x1b[31m' + (options.problem || 'disconnected') + '\x1b[m\r\n');
|
||||
term.cursorHidden = true;
|
||||
term.refresh(term.y, term.y);
|
||||
},
|
||||
|
||||
connectChannel: function () {
|
||||
var channel = this.props.channel;
|
||||
if (channel && channel.valid) {
|
||||
channel.addEventListener('message', this.onChannelMessage.bind(this));
|
||||
channel.addEventListener('close', this.onChannelClose.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
disconnectChannel: function () {
|
||||
if (this.props.channel) {
|
||||
this.props.channel.removeEventListener('message', this.onChannelMessage);
|
||||
this.props.channel.removeEventListener('close', this.onChannelClose);
|
||||
}
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
if (this.state.terminal)
|
||||
this.state.terminal.focus();
|
||||
},
|
||||
|
||||
onWindowResize: function () {
|
||||
if (this.refs) {
|
||||
var padding = 2 * 11;
|
||||
var node = this.getDOMNode();
|
||||
var terminal = this.refs.terminal.querySelector('.terminal');
|
||||
|
||||
var ch = document.createElement('div');
|
||||
ch.textContent = 'M';
|
||||
terminal.appendChild(ch);
|
||||
var height = ch.offsetHeight; // offsetHeight is only correct for block elements
|
||||
ch.style.display = 'inline';
|
||||
var width = ch.offsetWidth;
|
||||
terminal.removeChild(ch);
|
||||
|
||||
this.setState({
|
||||
rows: Math.floor((node.parentElement.clientHeight - padding) / height),
|
||||
cols: Math.floor((node.parentElement.clientWidth - padding) / width)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
send: function(value) {
|
||||
this.state.terminal.send(value);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { Terminal: Terminal };
|
||||
}());
|
||||
163
src/timer.css
Normal file
163
src/timer.css
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* This file is part of Cockpit.
|
||||
*
|
||||
* Copyright (C) 2015 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/>.
|
||||
*/
|
||||
#create-timer {
|
||||
display: none;
|
||||
}
|
||||
.vertical-scroll {
|
||||
max-height: 150px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.position-colon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div#boot {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
div#boot-or-specific-time {
|
||||
width: 170px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div#drop-time {
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input#boot-time {
|
||||
width: 50px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.hr, .min {
|
||||
width:30px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-inline {
|
||||
background: #f4f4f4;
|
||||
border-width: 0 1px 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: #bababa;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#boot-label {
|
||||
position: relative;
|
||||
right: 8px;
|
||||
white-space: nowrap;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
#repeat-time .form-inline:first-of-type {
|
||||
border-top: 1px solid #bababa;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="month-days"] {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="week-days"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="close"] {
|
||||
position: relative;
|
||||
float: right;
|
||||
right: 8px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#repeat-time [data-content="add"] {
|
||||
position: relative;
|
||||
float: right;
|
||||
right: 4px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#repeat-time [data-provide="datepicker"] {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
[data-content='day-error'].repeat-error {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #4d5258;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.has-error {
|
||||
border-color: #cc0000;
|
||||
}
|
||||
|
||||
.has-error:hover {
|
||||
border-color: #990000;
|
||||
}
|
||||
|
||||
.has-error:focus {
|
||||
border-color: #990000;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ff3333;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ff3333;
|
||||
}
|
||||
|
||||
.repeat-error {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #cc0000;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
#services-page .datepicker-dropdown .prev,
|
||||
#services-page .datepicker-dropdown .next {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.date {
|
||||
width:120px;
|
||||
}
|
||||
|
||||
#hr-error, #min-error {
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
position: relative;
|
||||
bottom: 6px;
|
||||
}
|
||||
|
||||
@media (min-width: 500px) {
|
||||
.cockpit-timer-modal-md {
|
||||
width: 500px;
|
||||
}
|
||||
.form-inline .form-control {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.form-inline .date .bootstrap-datepicker {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src";
|
|||
const builddir = (process.env.SRCDIR || __dirname);
|
||||
const distdir = builddir + path.sep + "dist";
|
||||
const section = process.env.ONLYDIR || null;
|
||||
const libdir = path.resolve(srcdir, "pkg" + path.sep + "lib");
|
||||
const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules");
|
||||
|
||||
/* A standard nodejs and webpack pattern */
|
||||
|
|
@ -21,13 +22,25 @@ var production = process.env.NODE_ENV === 'production';
|
|||
|
||||
var info = {
|
||||
entries: {
|
||||
"index": [
|
||||
"./index.es6"
|
||||
"recordings": [
|
||||
"./recordings.jsx",
|
||||
"./recordings.css",
|
||||
"./pkg/lib/listing.less",
|
||||
],
|
||||
"config": [
|
||||
"./config.jsx",
|
||||
"./recordings.css",
|
||||
]
|
||||
},
|
||||
files: [
|
||||
"index.html",
|
||||
"config.html",
|
||||
"player.jsx",
|
||||
"recordings.jsx",
|
||||
"recordings.css",
|
||||
"terminal.jsx",
|
||||
"manifest.json",
|
||||
"timer.css",
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -101,6 +114,12 @@ module.exports = {
|
|||
externals: externals,
|
||||
output: output,
|
||||
devtool: "source-map",
|
||||
resolve: {
|
||||
alias: {
|
||||
"fs": path.resolve(nodedir, "fs-extra"),
|
||||
},
|
||||
modules: [libdir, nodedir],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
|
|
@ -130,10 +149,18 @@ module.exports = {
|
|||
loader: 'babel-loader',
|
||||
test: /\.es6$/
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
loader: extract.extract("css-loader!less-loader")
|
||||
},
|
||||
{
|
||||
exclude: /node_modules/,
|
||||
loader: extract.extract('css-loader!sass-loader'),
|
||||
test: /\.scss$/
|
||||
},
|
||||
{
|
||||
loader: extract.extract("css-loader"),
|
||||
test: /\.css$/,
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue