Session recording module for Cockpit initial commit

This commit is contained in:
Kyrylo Gliebov 2018-07-02 18:03:59 +02:00
parent d73a91232e
commit a20e3c5a81
27 changed files with 5564 additions and 32 deletions

View file

@ -1,19 +1,19 @@
Name: cockpit-starter-kit Name: cockpit-session-recording
Version: @VERSION@ Version: @VERSION@
Release: 1%{?dist} Release: 1%{?dist}
Summary: Cockpit Starter Kit Example Module Summary: Cockpit Session Recording
License: LGPLv2+ License: LGPLv2+
Source: cockpit-starter-kit-%{version}.tar.gz Source: cockpit-session-recording-%{version}.tar.gz
BuildArch: noarch BuildArch: noarch
%define debug_package %{nil} %define debug_package %{nil}
%description %description
Cockpit Starter Kit Example Module Cockpit Session Recording
%prep %prep
%setup -n cockpit-starter-kit %setup -n cockpit-session-recording
%install %install
%make_install %make_install

View file

@ -1,7 +1,7 @@
<component type="addon"> <component type="addon">
<id>org.cockpit-project.starter-kit</id> <id>org.cockpit-project.session-recording</id>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<name>Starter Kit</name> <name>Session Recording</name>
<summary> <summary>
Scaffolding for a cockpit module. Scaffolding for a cockpit module.
</summary> </summary>
@ -11,5 +11,5 @@
</p> </p>
</description> </description>
<extends>cockpit.desktop</extends> <extends>cockpit.desktop</extends>
<launchable type="cockpit-manifest">cockpit-starter-kit</launchable> <launchable type="cockpit-manifest">cockpit-session-recording</launchable>
</component> </component>

View file

@ -1,5 +1,5 @@
{ {
"name": "starter-kit", "name": "session-recording",
"version": "0.1.0", "version": "0.1.0",
"description": "Scaffolding for a cockpit module", "description": "Scaffolding for a cockpit module",
"main": "index.js", "main": "index.js",
@ -34,6 +34,10 @@
"extract-text-webpack-plugin": "^4.0.0-beta.0", "extract-text-webpack-plugin": "^4.0.0-beta.0",
"htmlparser": "^1.7.7", "htmlparser": "^1.7.7",
"jed": "^1.1.1", "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", "po2json": "^0.4.5",
"sass-loader": "^7.0.3", "sass-loader": "^7.0.3",
"sizzle": "^2.3.3", "sizzle": "^2.3.3",
@ -46,5 +50,18 @@
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"react": "^16.4.2", "react": "^16.4.2",
"react-dom": "^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"
} }
} }

View file

@ -1,3 +0,0 @@
p {
font-weight: bold;
}

54
src/config.html Normal file
View 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
View 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'));
}());

View file

@ -1,5 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- <!--
This file is part of Cockpit.
Copyright (C) 2017 Red Hat, Inc. Copyright (C) 2017 Red Hat, Inc.
Cockpit is free software; you can redistribute it and/or modify it 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. Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License 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> <head>
<title translatable="yes">Cockpit Starter Kit</title> <title translate>Journal</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="description" content=""> <link href="../base1/patternfly.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1"> <link href="recordings.css" rel="stylesheet">
<script type="text/javascript" src="../base1/jquery.js"></script>
<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="../base1/cockpit.js"></script>
<script type="text/javascript" src="../*/po.js"></script> <script src="../*/po.js"></script>
<script type="text/javascript" src="index.js"></script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="view"/>
<script type="text/javascript" src="recordings.js"></script>
</body> </body>
</html> </html>

View file

@ -1,12 +1,15 @@
{ {
"version": "0.1", "version": "163.x",
"name": "session_recording",
"requires": { "requires": {
"cockpit": "137" "cockpit": "122"
}, },
"tools": { "menu": {
"index": { "index": {
"label": "Starter Kit" "label": "Session Recording",
"order": 100
} }
} }
} }

15
src/manifest.json.in Normal file
View file

@ -0,0 +1,15 @@
{
"version": "@VERSION@",
"name": "session_recording",
"requires": {
"cockpit": "122"
},
"menu": {
"index": {
"label": "Session Recording",
"order": 100
}
}
}

View 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
View 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
View 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
View 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;
}());

View file

@ -0,0 +1 @@
<div class="panel-heading">{{day}}</div>

View 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}}&#160;<i class="fa fa-caret-right"></i></span>
</div>
{{/count}}{{^count}}
<div class="cockpit-log-service" role="cell">{{service}}</div>
{{/count}}
</div>

View 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
View 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
View 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
View 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
View 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; }

View 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

File diff suppressed because it is too large Load diff

368
src/recordings.css Normal file
View 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
View 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
View 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
View 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;
}
}

View file

@ -14,6 +14,7 @@ const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src";
const builddir = (process.env.SRCDIR || __dirname); const builddir = (process.env.SRCDIR || __dirname);
const distdir = builddir + path.sep + "dist"; const distdir = builddir + path.sep + "dist";
const section = process.env.ONLYDIR || null; 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"); const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules");
/* A standard nodejs and webpack pattern */ /* A standard nodejs and webpack pattern */
@ -21,13 +22,25 @@ var production = process.env.NODE_ENV === 'production';
var info = { var info = {
entries: { entries: {
"index": [ "recordings": [
"./index.es6" "./recordings.jsx",
"./recordings.css",
"./pkg/lib/listing.less",
],
"config": [
"./config.jsx",
"./recordings.css",
] ]
}, },
files: [ files: [
"index.html", "index.html",
"config.html",
"player.jsx",
"recordings.jsx",
"recordings.css",
"terminal.jsx",
"manifest.json", "manifest.json",
"timer.css",
], ],
}; };
@ -101,6 +114,12 @@ module.exports = {
externals: externals, externals: externals,
output: output, output: output,
devtool: "source-map", devtool: "source-map",
resolve: {
alias: {
"fs": path.resolve(nodedir, "fs-extra"),
},
modules: [libdir, nodedir],
},
module: { module: {
rules: [ rules: [
{ {
@ -130,10 +149,18 @@ module.exports = {
loader: 'babel-loader', loader: 'babel-loader',
test: /\.es6$/ test: /\.es6$/
}, },
{
test: /\.less$/,
loader: extract.extract("css-loader!less-loader")
},
{ {
exclude: /node_modules/, exclude: /node_modules/,
loader: extract.extract('css-loader!sass-loader'), loader: extract.extract('css-loader!sass-loader'),
test: /\.scss$/ test: /\.scss$/
},
{
loader: extract.extract("css-loader"),
test: /\.css$/,
} }
] ]
}, },