Rebase and migration to full React instead of react-lite

This commit is contained in:
Kyrylo Gliebov 2018-09-14 18:34:33 +02:00
parent 4ca9b76b23
commit 1abe64fe0c
7 changed files with 2275 additions and 2246 deletions

View file

@ -42,27 +42,26 @@
"sass-loader": "^7.0.3", "sass-loader": "^7.0.3",
"sizzle": "^2.3.3", "sizzle": "^2.3.3",
"stdio": "^0.2.7", "stdio": "^0.2.7",
"webpack": "^4.17.1", "webpack": "^4.19.0",
"webpack-cli": "^3.1.0" "webpack-cli": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.0.0", "@babel/polyfill": "^7.0.0",
"node-sass": "^4.9.0",
"react": "^16.4.2",
"react-dom": "^16.4.2"
"bootstrap": "3.3.7", "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", "bootstrap-datetime-picker": "2.4.4",
"comment-json": "^1.1.3", "comment-json": "^1.1.3",
"term.js-cockpit": "0.0.10",
"fs.extra": "^1.3.2", "fs.extra": "^1.3.2",
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"ini": "^1.3.5", "ini": "^1.3.5",
"jquery": "3.3.1",
"moment": "2.22.2",
"mustache": "2.3.0",
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"raw-loader": "^0.5.1" "patternfly": "3.35.1",
"prop-types": "^15.6.2",
"raw-loader": "^0.5.1",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"term.js-cockpit": "0.0.10"
} }
} }

View file

@ -22,10 +22,11 @@
let cockpit = require("cockpit"); let cockpit = require("cockpit");
let React = require("react"); let React = require("react");
let ReactDOM = require("react-dom");
let json = require('comment-json'); let json = require('comment-json');
let ini = require('ini'); let ini = require('ini');
let Config = class extends React.Component { class Config extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
@ -38,7 +39,7 @@
config: null, config: null,
file_error: null, file_error: null,
submitting: "none", submitting: "none",
} };
} }
handleInputChange(e) { handleInputChange(e) {
@ -59,10 +60,8 @@
handleSubmit(event) { handleSubmit(event) {
this.setState({submitting:"block"}); this.setState({submitting:"block"});
console.log(event);
this.prepareConfig(); this.prepareConfig();
this.file.replace(this.state.config).done(() => { this.file.replace(this.state.config).done(() => {
console.log('updated');
this.setState({submitting:"none"}); this.setState({submitting:"none"});
}) })
.fail((error) => { .fail((error) => {
@ -72,20 +71,16 @@
} }
setConfig(data) { setConfig(data) {
console.log(data);
this.setState({config: data}); this.setState({config: data});
} }
fileReadFailed(reason) { fileReadFailed(reason) {
console.log(reason); console.log(reason);
this.setState({file_error: reason}); this.setState({file_error: reason});
console.log('failed to read file');
} }
componentDidMount() { componentDidMount() {
let parseFunc = function(data) { let parseFunc = function(data) {
console.log(data);
// return data;
return json.parse(data, null, true); return json.parse(data, null, true);
}; };
@ -106,8 +101,6 @@
// host: string // host: string
}); });
console.log(this.file);
let promise = this.file.read(); let promise = this.file.read();
promise.done((data) => { promise.done((data) => {
@ -253,10 +246,9 @@
</tr> </tr>
<tr> <tr>
<td className="top" /> <td className="top" />
<div className="spinner spinner-sm" style={{display: this.state.submitting}} />
<td> <td>
<button className="btn btn-default" type="submit">Save</button> <button className="btn btn-default" type="submit">Save</button>
<div className="spinner spinner-sm" style={{display: this.state.submitting}} />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -274,9 +266,9 @@
); );
} }
} }
}; }
let SssdConfig = class extends React.Component { class SssdConfig extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
@ -284,27 +276,24 @@
this.setConfig = this.setConfig.bind(this); this.setConfig = this.setConfig.bind(this);
this.file = null; this.file = null;
this.state = { this.state = {
config: { scope: "",
session_recording: { users: "",
scope: null, groups: "",
users: null, submitting: "none",
groups: null,
},
},
}; };
} }
handleInputChange(e) { handleInputChange(e) {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
const name = e.target.name; const name = e.target.name;
const config = this.state.config; const state = {};
config.session_recording[name] = value; state[name] = value;
this.setState(state);
this.forceUpdate();
} }
setConfig(data) { setConfig(data) {
this.setState({config: data}); const config = {...data['session_recording']};
this.setState(config);
} }
componentDidMount() { componentDidMount() {
@ -327,14 +316,21 @@
}); });
} }
handleSubmit() { handleSubmit(e) {
this.file.replace(this.state.config).done( function() { this.setState({submitting:"block"});
console.log('updated'); const obj = {};
obj.users = this.state.users;
obj.groups = this.state.groups;
obj.scope = this.state.scope;
obj['session_recording'] = this.state;
let _this = this;
this.file.replace(obj).done(function() {
_this.setState({submitting:"none"});
}) })
.fail(function(error) { .fail(function(error) {
console.log(error); console.log(error);
}); });
event.preventDefault(); e.preventDefault();
} }
render() { render() {
@ -346,7 +342,7 @@
<td><label htmlFor="scope">Scope</label></td> <td><label htmlFor="scope">Scope</label></td>
<td> <td>
<select name="scope" id="scope" className="form-control" <select name="scope" id="scope" className="form-control"
value={this.state.config.session_recording.scope} value={this.state.scope}
onChange={this.handleInputChange} > onChange={this.handleInputChange} >
<option value="none">None</option> <option value="none">None</option>
<option value="some">Some</option> <option value="some">Some</option>
@ -358,24 +354,23 @@
<td><label htmlFor="users">Users</label></td> <td><label htmlFor="users">Users</label></td>
<td> <td>
<input type="text" id="users" name="users" <input type="text" id="users" name="users"
value={this.state.config.session_recording.users} value={this.state.users}
className="form-control" /> className="form-control" onChange={this.handleInputChange} />
</td> </td>
</tr> </tr>
<tr> <tr>
<td><label htmlFor="groups">Groups</label></td> <td><label htmlFor="groups">Groups</label></td>
<td> <td>
<input type="text" id="groups" name="groups" <input type="text" id="groups" name="groups"
value={this.state.config.session_recording.groups} value={this.state.groups}
className="form-control" onChange={this.handleInputChange} /> className="form-control" onChange={this.handleInputChange} />
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td />
</td>
<td> <td>
<button className="btn btn-default" type="submit">Save</button> <button className="btn btn-default" type="submit">Save</button>
<span className="spinner spinner-sm" style={{display: this.state.submitting}} />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -383,8 +378,8 @@
</form> </form>
); );
} }
}; }
React.render(<Config />, document.getElementById('sr_config')); ReactDOM.render(<Config />, document.getElementById('sr_config'));
React.render(<SssdConfig />, document.getElementById('sssd_config')); ReactDOM.render(<SssdConfig />, document.getElementById('sssd_config'));
}()); }());

View file

@ -17,11 +17,9 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>. * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/ */
"use strict"; import PropTypes from 'prop-types';
import React from 'react';
var React = require('react'); import './listing.less';
require('./listing.less');
/* entry for an alert in the listing, can be expanded (with details) or standard /* 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> * rowId optional: an identifier for the row which will be set as "data-row-id" attribute on the <tr>
@ -47,55 +45,43 @@ require('./listing.less');
* initiallyExpanded optional: the entry will be initially rendered as expanded, but then behaves normally * 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 * expandChanged optional: callback will be used if the row is either expanded or collapsed passing single `isExpanded` boolean argument
*/ */
var ListingRow = React.createClass({ export class ListingRow extends React.Component {
propTypes: { constructor(props) {
rowId: React.PropTypes.string, super(props);
columns: React.PropTypes.array.isRequired, this.state = {
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 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 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 loadedTabs: {}, // which tabs were already loaded - this is important for 'loadOnDemand' setting
// contains tab indices // contains tab indices
selected: this.props.selected, // whether the current row is selected selected: this.props.selected, // whether the current row is selected
}; };
}, this.handleNavigateClick = this.handleNavigateClick.bind(this);
handleNavigateClick: function(e) { this.handleExpandClick = this.handleExpandClick.bind(this);
this.handleSelectClick = this.handleSelectClick.bind(this);
this.handleTabClick = this.handleTabClick.bind(this);
}
handleNavigateClick(e) {
// only consider primary mouse button // only consider primary mouse button
if (!e || e.button !== 0) if (!e || e.button !== 0)
return; return;
this.props.navigateToItem(); this.props.navigateToItem();
}, }
handleExpandClick: function(e) {
handleExpandClick(e) {
// only consider primary mouse button // only consider primary mouse button
if (!e || e.button !== 0) if (!e || e.button !== 0)
return; return;
var willBeExpanded = !this.state.expanded && this.props.tabRenderers.length > 0; let willBeExpanded = !this.state.expanded && this.props.tabRenderers.length > 0;
this.setState({ expanded: willBeExpanded }); this.setState({ expanded: willBeExpanded });
var loadedTabs = {}; let loadedTabs = {};
// unload all tabs if not expanded // unload all tabs if not expanded
if (willBeExpanded) { if (willBeExpanded) {
// see if we should preload some tabs // see if we should preload some tabs
var tabIdx; let tabIdx;
var tabPresence; let tabPresence;
for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) { for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) {
if ('presence' in this.props.tabRenderers[tabIdx]) if ('presence' in this.props.tabRenderers[tabIdx])
tabPresence = this.props.tabRenderers[tabIdx].presence; tabPresence = this.props.tabRenderers[tabIdx].presence;
@ -115,13 +101,14 @@ var ListingRow = React.createClass({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}, }
handleSelectClick: function(e) {
handleSelectClick(e) {
// only consider primary mouse button // only consider primary mouse button
if (!e || e.button !== 0) if (!e || e.button !== 0)
return; return;
var selected = !this.state.selected; let selected = !this.state.selected;
this.setState({ selected: selected }); this.setState({ selected: selected });
if (this.props.selectChanged) if (this.props.selectChanged)
@ -129,14 +116,15 @@ var ListingRow = React.createClass({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}, }
handleTabClick: function(tabIdx, e) {
handleTabClick(tabIdx, e) {
// only consider primary mouse button // only consider primary mouse button
if (!e || e.button !== 0) if (!e || e.button !== 0)
return; return;
var prevTab = this.state.activeTab; let prevTab = this.state.activeTab;
var prevTabPresence = 'default'; let prevTabPresence = 'default';
var loadedTabs = this.state.loadedTabs; let loadedTabs = this.state.loadedTabs;
if (prevTab !== tabIdx) { if (prevTab !== tabIdx) {
// see if we need to unload the previous tab // see if we need to unload the previous tab
if ('presence' in this.props.tabRenderers[prevTab]) if ('presence' in this.props.tabRenderers[prevTab])
@ -151,41 +139,42 @@ var ListingRow = React.createClass({
} }
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); 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) { render() {
let self = this;
// only enable navigation if a function is provided and the row isn't expanded (prevent accidental navigation)
let allowNavigate = !!this.props.navigateToItem && !this.state.expanded;
let headerEntries = this.props.columns.map((itm, index) => {
if (typeof itm === 'string' || typeof itm === 'number' || itm === null || itm === undefined || itm instanceof String || React.isValidElement(itm)) if (typeof itm === 'string' || typeof itm === 'number' || itm === null || itm === undefined || itm instanceof String || React.isValidElement(itm))
return (<td>{itm}</td>); return (<td key={index}>{itm}</td>);
else if ('header' in itm && itm.header) else if ('header' in itm && itm.header)
return (<th>{itm.name}</th>); return (<th key={index}>{itm.name}</th>);
else if ('tight' in itm && itm.tight) else if ('tight' in itm && itm.tight)
return (<td className="listing-ct-actions">{itm.name || itm.element}</td>); return (<td key={index} className="listing-ct-actions">{itm.name || itm.element}</td>);
else else
return (<td>{itm.name}</td>); return (<td key={index}>{itm.name}</td>);
}); });
var allowExpand = (this.props.tabRenderers.length > 0); let allowExpand = (this.props.tabRenderers.length > 0);
var expandToggle; let expandToggle;
if (allowExpand) { if (allowExpand) {
expandToggle = <td className="listing-ct-toggle" onClick={ allowNavigate ? this.handleExpandClick : undefined }> expandToggle = <td key="expandToggle" className="listing-ct-toggle" onClick={ allowNavigate ? this.handleExpandClick : undefined }>
<i className="fa fa-fw" /> <i className="fa fa-fw" />
</td>; </td>;
} else { } else {
expandToggle = <td className="listing-ct-toggle" />; expandToggle = <td key="expandToggle-empty" className="listing-ct-toggle" />;
} }
var listingItemClasses = ["listing-ct-item"]; let listingItemClasses = ["listing-ct-item"];
if (!allowNavigate) if (!allowNavigate)
listingItemClasses.push("listing-ct-nonavigate"); listingItemClasses.push("listing-ct-nonavigate");
if (!allowExpand) if (!allowExpand)
listingItemClasses.push("listing-ct-noexpand"); listingItemClasses.push("listing-ct-noexpand");
var allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined); let allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined);
var clickHandler; let clickHandler;
if (allowSelect) { if (allowSelect) {
clickHandler = this.handleSelectClick; clickHandler = this.handleSelectClick;
if (this.state.selected) if (this.state.selected)
@ -197,7 +186,7 @@ var ListingRow = React.createClass({
clickHandler = this.handleExpandClick; clickHandler = this.handleExpandClick;
} }
var listingItem = ( let listingItem = (
<tr data-row-id={ this.props.rowId } <tr data-row-id={ this.props.rowId }
className={ listingItemClasses.join(' ') } className={ listingItemClasses.join(' ') }
onClick={clickHandler}> onClick={clickHandler}>
@ -207,18 +196,18 @@ var ListingRow = React.createClass({
); );
if (this.state.expanded) { if (this.state.expanded) {
var links = this.props.tabRenderers.map(function(itm, idx) { let links = this.props.tabRenderers.map((itm, idx) => {
return ( return (
<li key={idx} className={ (idx === self.state.activeTab) ? "active" : ""} > <li key={idx} className={ (idx === self.state.activeTab) ? "active" : ""} >
<a href="#" tabIndex="0" onClick={ self.handleTabClick.bind(self, idx) }>{itm.name}</a> <a href="#" tabIndex="0" onClick={ self.handleTabClick.bind(self, idx) }>{itm.name}</a>
</li> </li>
); );
}); });
var tabs = []; let tabs = [];
var tabIdx; let tabIdx;
var Renderer; let Renderer;
var rendererData; let rendererData;
var row; let row;
for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) { for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) {
Renderer = this.props.tabRenderers[tabIdx].renderer; Renderer = this.props.tabRenderers[tabIdx].renderer;
rendererData = this.props.tabRenderers[tabIdx].data; rendererData = this.props.tabRenderers[tabIdx].data;
@ -231,7 +220,7 @@ var ListingRow = React.createClass({
tabs.push(<div className="listing-ct-body" key={tabIdx} hidden>{row}</div>); tabs.push(<div className="listing-ct-body" key={tabIdx} hidden>{row}</div>);
} }
var listingDetail; let listingDetail;
if ('listingDetail' in this.props) { if ('listingDetail' in this.props) {
listingDetail = ( listingDetail = (
<span className="listing-ct-caption"> <span className="listing-ct-caption">
@ -268,8 +257,26 @@ var ListingRow = React.createClass({
); );
} }
} }
}); }
ListingRow.defaultProps = {
tabRenderers: [],
navigateToItem: null,
};
ListingRow.propTypes = {
rowId: PropTypes.string,
columns: PropTypes.array.isRequired,
tabRenderers: PropTypes.array,
navigateToItem: PropTypes.func,
listingDetail: PropTypes.node,
listingActions: PropTypes.arrayOf(PropTypes.node),
selectChanged: PropTypes.func,
selected: PropTypes.bool,
initiallyExpanded: PropTypes.bool,
expandChanged: PropTypes.func,
initiallyActiveTab: PropTypes.bool
};
/* Implements a PatternFly 'List View' pattern /* Implements a PatternFly 'List View' pattern
* https://www.patternfly.org/list-view/ * https://www.patternfly.org/list-view/
* Properties: * Properties:
@ -281,44 +288,27 @@ var ListingRow = React.createClass({
* receives the column index as argument * receives the column index as argument
* - actions: additional listing-wide actions (displayed next to the list's title) * - actions: additional listing-wide actions (displayed next to the list's title)
*/ */
var Listing = React.createClass({ export const Listing = (props) => {
propTypes: { let bodyClasses = ["listing", "listing-ct"];
title: React.PropTypes.string.isRequired, if (props.fullWidth)
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"); bodyClasses.push("listing-ct-wide");
var headerClasses; let headerClasses;
var headerRow; let headerRow;
var selectableRows; let selectableRows;
if (!this.props.children || this.props.children.length === 0) { if (!props.children || props.children.length === 0) {
headerClasses = "listing-ct-empty"; headerClasses = "listing-ct-empty";
headerRow = <tr><td>{this.props.emptyCaption}</td></tr>; headerRow = <tr><td>{props.emptyCaption}</td></tr>;
} else if (this.props.columnTitles.length) { } else if (props.columnTitles.length) {
// check if any of the children are selectable // check if any of the children are selectable
selectableRows = false; selectableRows = false;
this.props.children.forEach(function(r) { props.children.forEach(function(r) {
if (r.props.selected !== undefined) if (r.props.selected !== undefined)
selectableRows = true; selectableRows = true;
}); });
if (selectableRows) { if (selectableRows) {
// now make sure that if one is set, it's available on all items // now make sure that if one is set, it's available on all items
this.props.children.forEach(function(r) { props.children.forEach(function(r) {
if (r.props.selected === undefined) if (r.props.selected === undefined)
r.props.selected = false; r.props.selected = false;
}); });
@ -326,21 +316,21 @@ var Listing = React.createClass({
headerRow = ( headerRow = (
<tr> <tr>
<th className="listing-ct-toggle" /> <th key="empty" className="listing-ct-toggle" />
{ this.props.columnTitles.map(function (title, index) { { props.columnTitles.map((title, index) => {
var clickHandler = null; let clickHandler = null;
if (self.props.columnTitleClick) if (props.columnTitleClick)
clickHandler = function() { self.props.columnTitleClick(index) }; clickHandler = function() { props.columnTitleClick(index) };
return <th onClick={clickHandler}>{title}</th>; return <th key={index} onClick={clickHandler}>{title}</th>;
}) } }) }
</tr> </tr>
); );
} else { } else {
headerRow = <tr /> headerRow = <tr />;
} }
var caption; let caption;
if (this.props.title || (this.props.actions && this.props.actions.length > 0)) if (props.title || (props.actions && props.actions.length > 0))
caption = <caption className="cockpit-caption">{this.props.title}{this.props.actions}</caption>; caption = <caption className="cockpit-caption">{props.title}{props.actions}</caption>;
return ( return (
<table className={ bodyClasses.join(" ") }> <table className={ bodyClasses.join(" ") }>
@ -348,13 +338,27 @@ var Listing = React.createClass({
<thead className={headerClasses}> <thead className={headerClasses}>
{headerRow} {headerRow}
</thead> </thead>
{this.props.children} {props.children}
</table> </table>
); );
}, };
});
Listing.defaultProps = {
module.exports = { title: '',
ListingRow: ListingRow, fullWidth: true,
Listing: Listing, columnTitles: [],
actions: []
};
Listing.propTypes = {
title: PropTypes.string,
fullWidth: PropTypes.bool,
emptyCaption: PropTypes.string.isRequired,
columnTitles: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
])),
columnTitleClick: PropTypes.func,
actions: PropTypes.arrayOf(PropTypes.node)
}; };

View file

@ -16,17 +16,13 @@
* 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 Cockpit; If not, see <http://www.gnu.org/licenses/>. * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/ */
(function() {
"use strict"; "use strict";
import React from 'react';
let cockpit = require("cockpit"); let cockpit = require("cockpit");
let _ = cockpit.gettext; let _ = cockpit.gettext;
let React = require("react");
let Term = require("term.js-cockpit"); let Term = require("term.js-cockpit");
let Journal = require("journal"); let Journal = require("journal");
let $ = require("jquery"); let $ = require("jquery");
require("console.css"); require("console.css");
/* /*
@ -42,14 +38,14 @@
throw Error("invalid \"" + field + "\" field type: " + typeof (value)); throw Error("invalid \"" + field + "\" field type: " + typeof (value));
} }
return value; return value;
} };
let scrollToBottom = function(id) { let scrollToBottom = function(id) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) { if (el) {
el.scrollTop = el.scrollHeight; el.scrollTop = el.scrollHeight;
} }
} };
/* /*
* An auto-loading buffer of recording's packets. * An auto-loading buffer of recording's packets.
@ -209,7 +205,7 @@
addPacket(pkt) { addPacket(pkt) {
/* TODO Validate the packet */ /* TODO Validate the packet */
/* Add the packet */ /* Add the packet */
this.pktList.push(pkt) this.pktList.push(pkt);
/* Notify any matching listeners */ /* Notify any matching listeners */
while (this.idxDfdList.length > 0) { while (this.idxDfdList.length > 0) {
let idxDfd = this.idxDfdList[0]; let idxDfd = this.idxDfdList[0];
@ -469,7 +465,7 @@
jumpTo(e) { jumpTo(e) {
if (this.props.fastForwardFunc) { if (this.props.fastForwardFunc) {
let percent = parseInt((e.offsetX * 100) / e.currentTarget.clientWidth); let percent = parseInt((e.nativeEvent.offsetX * 100) / e.currentTarget.clientWidth);
let ts = parseInt((this.props.length * percent) / 100); let ts = parseInt((this.props.length * percent) / 100);
this.props.fastForwardFunc(ts); this.props.fastForwardFunc(ts);
} }
@ -489,10 +485,6 @@
}; };
let InputPlayer = class extends React.Component { let InputPlayer = class extends React.Component {
constructor(props) {
super(props);
}
render() { render() {
return ( return (
<div id="input-player" className="panel panel-default"> <div id="input-player" className="panel panel-default">
@ -500,15 +492,14 @@
<span>Input</span> <span>Input</span>
</div> </div>
<div className="panel-body"> <div className="panel-body">
<textarea name="input" id="input-textarea" cols="30" rows="10" readonly disabled>{this.props.input}</textarea> <textarea name="input" id="input-textarea" cols="30" rows="10" value={this.props.input} readOnly disabled />
</div> </div>
</div> </div>
); );
} }
}; };
let Player = class extends React.Component { export class Player extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -558,7 +549,8 @@
currentTsPost: 0, currentTsPost: 0,
scale: 1, scale: 1,
error: null, error: null,
input: "" input: "",
mark: 0,
}; };
this.containerHeight = 290; this.containerHeight = 290;
@ -608,6 +600,7 @@
/* Move to beginning of recording */ /* Move to beginning of recording */
this.recTS = 0; this.recTS = 0;
this.setState({currentTsPost: parseInt(this.recTS)});
/* Start the playback time */ /* Start the playback time */
this.locTS = performance.now(); this.locTS = performance.now();
@ -782,6 +775,7 @@
/* Send packet ts to the top */ /* Send packet ts to the top */
this.props.onTsChange(this.pkt.pos); this.props.onTsChange(this.pkt.pos);
this.setState({currentTsPost: parseInt(this.pkt.pos)});
/* Output the packet */ /* Output the packet */
if (this.pkt.is_io && !this.pkt.is_output) { if (this.pkt.is_io && !this.pkt.is_output) {
@ -823,7 +817,7 @@
} }
clearInputPlayer() { clearInputPlayer() {
this.setState({input: null}); this.setState({input: ""});
} }
rewindToStart() { rewindToStart() {
@ -978,6 +972,9 @@
if (this.state.input != prevState.input) { if (this.state.input != prevState.input) {
scrollToBottom("input-textarea"); scrollToBottom("input-textarea");
} }
if (prevProps.logsTs != this.props.logsTs) {
this.fastForwardToTS(this.props.logsTs);
}
} }
render() { render() {
@ -995,7 +992,7 @@
const style = { const style = {
"transform": "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")", "transform": "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")",
"transform-origin": "top left", "transformOrigin": "top left",
"display": "inline-block", "display": "inline-block",
"margin": "0 auto", "margin": "0 auto",
"position": "absolute", "position": "absolute",
@ -1004,9 +1001,9 @@
}; };
const scrollwrap = { const scrollwrap = {
"min-width": "630px", "minWidth": "630px",
"height": this.containerHeight + "px", "height": this.containerHeight + "px",
"background-color": "#f5f5f5", "backgroundColor": "#f5f5f5",
"overflow": this.state.term_scroll, "overflow": this.state.term_scroll,
"position": "relative", "position": "relative",
}; };
@ -1016,7 +1013,7 @@
}; };
const progressbar_style = { const progressbar_style = {
'margin-top': '10px', 'marginTop': '10px',
}; };
const currentTsPost = function(currentTS, bufLength) { const currentTsPost = function(currentTS, bufLength) {
@ -1121,12 +1118,4 @@
window.removeEventListener("keydown", this.handleKeyDown, false); window.removeEventListener("keydown", this.handleKeyDown, false);
this.state.term.destroy(); this.state.term.destroy();
} }
}; }
Player.propTypes = {
matchList: React.PropTypes.array,
// onTitleChanged: React.PropTypes.func
};
module.exports = { Player: Player };
}());

View file

@ -16,16 +16,16 @@
* 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 Cockpit; If not, see <http://www.gnu.org/licenses/>. * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/ */
(function() {
"use strict"; "use strict";
import React from "react";
import ReactDOM from "react-dom";
let $ = require("jquery"); let $ = require("jquery");
let cockpit = require("cockpit"); let cockpit = require("cockpit");
let _ = cockpit.gettext; let _ = cockpit.gettext;
let moment = require("moment"); let moment = require("moment");
let Journal = require("journal"); let Journal = require("journal");
let React = require("react");
let Listing = require("cockpit-components-listing.jsx"); let Listing = require("cockpit-components-listing.jsx");
let Player = require("./player.jsx"); let Player = require("./player.jsx");
@ -44,7 +44,7 @@
s = '0' + s; s = '0' + s;
} }
return ((i < 0) ? '-' : '') + s; return ((i < 0) ? '-' : '') + s;
} };
/* /*
* Format date and time for a number of milliseconds since Epoch. * Format date and time for a number of milliseconds since Epoch.
@ -107,7 +107,7 @@
} }
return false; return false;
} };
/* /*
* A component representing a date & time picker based on bootstrap-datetime-picker. * A component representing a date & time picker based on bootstrap-datetime-picker.
@ -116,7 +116,7 @@
* - onDateChange: function to call on date change event of datepicker. * - onDateChange: function to call on date change event of datepicker.
* - date: variable to pass which will be used as initial value. * - date: variable to pass which will be used as initial value.
*/ */
let Datetimepicker = class extends React.Component { class Datetimepicker extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleDateChange = this.handleDateChange.bind(this); this.handleDateChange = this.handleDateChange.bind(this);
@ -203,20 +203,25 @@
* A component representing a username input text field. * A component representing a username input text field.
* TODO make as a select / drop-down with list of exisiting users. * TODO make as a select / drop-down with list of exisiting users.
*/ */
let UserPicker = class extends React.Component { class UserPicker extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleUsernameChange = this.handleUsernameChange.bind(this); this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.state = {
username: cockpit.location.options.username || "",
};
} }
handleUsernameChange(e) { handleUsernameChange(e) {
this.props.onUsernameChange(e.target.value); const value = e.target.value;
this.setState({username: value});
this.props.onUsernameChange(value);
} }
render() { render() {
return ( return (
<div className="input-group"> <div className="input-group">
<input type="text" className="form-control" value={this.props.username} <input type="text" className="form-control" value={this.state.username}
onChange={this.handleUsernameChange} /> onChange={this.handleUsernameChange} />
</div> </div>
); );
@ -227,38 +232,60 @@
constructor(props) { constructor(props) {
super(props); super(props);
this.handleHostnameChange = this.handleHostnameChange.bind(this); this.handleHostnameChange = this.handleHostnameChange.bind(this);
this.state = {
hostname: cockpit.location.options.hostname || "",
};
} }
handleHostnameChange(e) { handleHostnameChange(e) {
this.props.onHostnameChange(e.target.value); const value = e.target.value;
this.setState({hostname: value});
this.props.onHostnameChange(value);
} }
render() { render() {
return ( return (
<div className="input-group"> <div className="input-group">
<input type="text" className="form-control" value={this.props.hostname} <input type="text" className="form-control" value={this.state.hostname}
onChange={this.handleHostnameChange} /> onChange={this.handleHostnameChange} />
</div> </div>
); );
} }
} };
function LogElement(props) { function LogElement(props) {
const entry = props.entry; const entry = props.entry;
const start = props.start; const start = props.start;
const end = props.end; const end = props.end;
const entry_timestamp = entry.__REALTIME_TIMESTAMP / 1000; const cursor = entry.__CURSOR;
const entry_timestamp = parseInt(entry.__REALTIME_TIMESTAMP / 1000);
const timeClick = function(e) {
const ts = entry_timestamp - start;
if (ts > 0) {
props.jumpToTs(ts);
} else {
props.jumpToTs(0);
}
};
const messageClick = () => {
const url = '/system/logs#/' + cursor + '?parent_options={}';
const win = window.open(url, '_blank');
win.focus();
};
let className = 'cockpit-logline'; let className = 'cockpit-logline';
if (start < entry_timestamp && end > entry_timestamp) { if (start < entry_timestamp && end > entry_timestamp) {
className = 'cockpit-logline highlighted'; className = 'cockpit-logline highlighted';
} }
return ( return (
<div className={className} data-cursor={entry.__CURSOR}> <div className={className} data-cursor={cursor} key={cursor}>
<div className="cockpit-log-warning"> <div className="cockpit-log-warning">
<i className="fa fa-exclamation-triangle" /> <i className="fa fa-exclamation-triangle" />
</div> </div>
<div className="logs-view-log-time">{formatDateTime(parseInt(entry.__REALTIME_TIMESTAMP / 1000))}</div> <div className="logs-view-log-time" onClick={timeClick}>{formatDateTime(entry_timestamp)}</div>
<span className="cockpit-log-message">{entry.MESSAGE}</span> <span className="cockpit-log-message" onClick={messageClick}>{entry.MESSAGE}</span>
</div> </div>
); );
} }
@ -268,7 +295,7 @@
const start = props.start; const start = props.start;
const end = props.end; const end = props.end;
const rows = entries.map((entry) => const rows = entries.map((entry) =>
<LogElement entry={entry} start={start} end={end} /> <LogElement key={entry.__CURSOR} entry={entry} start={start} end={end} jumpToTs={props.jumpToTs} />
); );
return ( return (
<div className="panel panel-default cockpit-log-panel" id="logs-view"> <div className="panel panel-default cockpit-log-panel" id="logs-view">
@ -277,7 +304,7 @@
); );
} }
let Logs = class extends React.Component { class Logs extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.journalctlError = this.journalctlError.bind(this); this.journalctlError = this.journalctlError.bind(this);
@ -409,7 +436,7 @@
<button className="btn btn-default" style={{"float":"right"}} onClick={this.loadEarlier}>Load earlier entries</button> <button className="btn btn-default" style={{"float":"right"}} onClick={this.loadEarlier}>Load earlier entries</button>
</div> </div>
<LogsView entries={this.state.entries} start={this.props.recording.start} <LogsView entries={this.state.entries} start={this.props.recording.start}
end={this.props.recording.end} /> end={this.props.recording.end} jumpToTs={this.props.jumpToTs} />
<div className="panel-heading"> <div className="panel-heading">
<button className="btn btn-default" onClick={this.loadLater}>Load later entries</button> <button className="btn btn-default" onClick={this.loadLater}>Load later entries</button>
</div> </div>
@ -427,7 +454,7 @@
* - recording: either null for no recording data available yet, or a * - recording: either null for no recording data available yet, or a
* recording object, as created by the View below. * recording object, as created by the View below.
*/ */
let Recording = class extends React.Component { class Recording extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.goBackToList = this.goBackToList.bind(this); this.goBackToList = this.goBackToList.bind(this);
@ -482,6 +509,7 @@
(<Player.Player (<Player.Player
ref="player" ref="player"
matchList={this.props.recording.matchList} matchList={this.props.recording.matchList}
logsTs={this.props.logsTs}
onTsChange={this.props.onTsChange} />); onTsChange={this.props.onTsChange} />);
return ( return (
@ -502,6 +530,7 @@
</div> </div>
<div className="panel-body"> <div className="panel-body">
<table className="form-table-ct"> <table className="form-table-ct">
<tbody>
<tr> <tr>
<td>{_("ID")}</td> <td>{_("ID")}</td>
<td>{r.id}</td> <td>{r.id}</td>
@ -535,6 +564,7 @@
<td>{_("User")}</td> <td>{_("User")}</td>
<td>{r.user}</td> <td>{r.user}</td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@ -545,14 +575,14 @@
); );
} }
} }
}; }
/* /*
* A component representing a list of recordings. * A component representing a list of recordings.
* Properties: * Properties:
* - list: an array with recording objects, as created by the View below * - list: an array with recording objects, as created by the View below
*/ */
let RecordingList = class extends React.Component { class RecordingList extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.handleColumnClick = this.handleColumnClick.bind(this); this.handleColumnClick = this.handleColumnClick.bind(this);
@ -618,17 +648,17 @@
getColumnTitles() { getColumnTitles() {
let columnTitles = [ let columnTitles = [
(<div id="user" className="sort" onClick={this.handleColumnClick}><span>{_("User")}</span> <div (<div id="user" className="sort" onClick={this.handleColumnClick}><span>{_("User")}</span> <div
ref="user" className="sort-icon"></div></div>), ref="user" className="sort-icon" /></div>),
(<div id="start" className="sort" onClick={this.handleColumnClick}><span>{_("Start")}</span> <div (<div id="start" className="sort" onClick={this.handleColumnClick}><span>{_("Start")}</span> <div
ref="start" className="sort-icon"></div></div>), ref="start" className="sort-icon" /></div>),
(<div id="end" className="sort" onClick={this.handleColumnClick}><span>{_("End")}</span> <div (<div id="end" className="sort" onClick={this.handleColumnClick}><span>{_("End")}</span> <div
ref="end" className="sort-icon"></div></div>), ref="end" className="sort-icon" /></div>),
(<div id="duration" className="sort" onClick={this.handleColumnClick}><span>{_("Duration")}</span> <div (<div id="duration" className="sort" onClick={this.handleColumnClick}><span>{_("Duration")}</span> <div
ref="duration" className="sort-icon"></div></div>), ref="duration" className="sort-icon" /></div>),
]; ];
if (this.props.diff_hosts === true) { if (this.props.diff_hosts === true) {
columnTitles.push((<div id="hostname" className="sort" onClick={this.handleColumnClick}> columnTitles.push((<div id="hostname" className="sort" onClick={this.handleColumnClick}>
<span>{_("Hostname")}</span> <div ref="hostname" className="sort-icon"></div></div>)); <span>{_("Hostname")}</span> <div ref="hostname" className="sort-icon" /></div>));
} }
return columnTitles; return columnTitles;
} }
@ -637,7 +667,7 @@
let columns = [r.user, let columns = [r.user,
formatDateTime(r.start), formatDateTime(r.start),
formatDateTime(r.end), formatDateTime(r.end),
formatDuration(r.end - r.start)] formatDuration(r.end - r.start)];
if (this.props.diff_hosts === true) { if (this.props.diff_hosts === true) {
columns.push(r.hostname); columns.push(r.hostname);
} }
@ -653,6 +683,7 @@
let r = list[i]; let r = list[i];
let columns = this.getColumns(r); let columns = this.getColumns(r);
rows.push(<Listing.ListingRow rows.push(<Listing.ListingRow
key={r.id}
rowId={r.id} rowId={r.id}
columns={columns} columns={columns}
navigateToItem={this.navigateToRecording.bind(this, r)} />); navigateToItem={this.navigateToRecording.bind(this, r)} />);
@ -661,7 +692,8 @@
<div> <div>
<div className="content-header-extra"> <div className="content-header-extra">
<table className="form-table-ct"> <table className="form-table-ct">
<th> <thead>
<tr>
<td className="top"> <td className="top">
<label className="control-label" htmlFor="dateSince">Since</label> <label className="control-label" htmlFor="dateSince">Since</label>
</td> </td>
@ -680,8 +712,7 @@
<label className="control-label" htmlFor="username">Username</label> <label className="control-label" htmlFor="username">Username</label>
</td> </td>
<td> <td>
<UserPicker onUsernameChange={this.props.onUsernameChange} <UserPicker onUsernameChange={this.props.onUsernameChange} />
username={this.props.username} />
</td> </td>
<td className="top"> <td className="top">
<label className="control-label" htmlFor="hostname">Hostname</label> <label className="control-label" htmlFor="hostname">Hostname</label>
@ -697,7 +728,8 @@
<a href="/cockpit/@localhost/session-recording/config.html" className="btn btn-default" data-toggle="modal"> <a href="/cockpit/@localhost/session-recording/config.html" className="btn btn-default" data-toggle="modal">
<i className="fa fa-cog" aria-hidden="true" /></a> <i className="fa fa-cog" aria-hidden="true" /></a>
</td> </td>
</th> </tr>
</thead>
</table> </table>
</div> </div>
<Listing.Listing title={_("Sessions")} <Listing.Listing title={_("Sessions")}
@ -709,14 +741,14 @@
</div> </div>
); );
} }
}; }
/* /*
* A component representing the view upon a list of recordings, or a * A component representing the view upon a list of recordings, or a
* single recording. Extracts the ID of the recording to display from * single recording. Extracts the ID of the recording to display from
* cockpit.location.path[0]. If it's zero, displays the list. * cockpit.location.path[0]. If it's zero, displays the list.
*/ */
let View = class extends React.Component { class View extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.onLocationChanged = this.onLocationChanged.bind(this); this.onLocationChanged = this.onLocationChanged.bind(this);
@ -726,6 +758,7 @@
this.handleUsernameChange = this.handleUsernameChange.bind(this); this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.handleHostnameChange = this.handleHostnameChange.bind(this); this.handleHostnameChange = this.handleHostnameChange.bind(this);
this.handleTsChange = this.handleTsChange.bind(this); this.handleTsChange = this.handleTsChange.bind(this);
this.handleLogTsChange = this.handleLogTsChange.bind(this);
/* Journalctl instance */ /* Journalctl instance */
this.journalctl = null; this.journalctl = null;
/* Recording ID journalctl instance is invoked with */ /* Recording ID journalctl instance is invoked with */
@ -744,12 +777,13 @@
dateUntil: cockpit.location.options.dateUntil || null, dateUntil: cockpit.location.options.dateUntil || null,
dateUntilLastValid: null, dateUntilLastValid: null,
/* value to filter recordings by username */ /* value to filter recordings by username */
username: cockpit.location.options.username || null, username: cockpit.location.options.username || "",
hostname: cockpit.location.options.hostname || null, hostname: cockpit.location.options.hostname || "",
error_tlog_uid: false, error_tlog_uid: false,
diff_hosts: false, diff_hosts: false,
curTs: null, curTs: null,
} logsTs: null,
};
} }
/* /*
@ -949,6 +983,10 @@
this.setState({curTs: ts}); this.setState({curTs: ts});
} }
handleLogTsChange(ts) {
this.setState({logsTs: ts});
}
componentDidMount() { componentDidMount() {
let proc = cockpit.spawn(["getent", "passwd", "tlog"]); let proc = cockpit.spawn(["getent", "passwd", "tlog"]);
@ -1026,11 +1064,12 @@
} else { } else {
return ( return (
<div> <div>
<Recording recording={this.recordingMap[this.state.recordingID]} onTsChange={this.handleTsChange} /> <Recording recording={this.recordingMap[this.state.recordingID]} onTsChange={this.handleTsChange} logsTs={this.state.logsTs} />
<div className="container-fluid"> <div className="container-fluid">
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
<Logs recording={this.recordingMap[this.state.recordingID]} curTs={this.state.curTs} /> <Logs recording={this.recordingMap[this.state.recordingID]} curTs={this.state.curTs}
jumpToTs={this.handleLogTsChange} />
</div> </div>
</div> </div>
</div> </div>
@ -1038,7 +1077,6 @@
); );
} }
} }
}; }
React.render(<View />, document.getElementById('view')); ReactDOM.render(<View />, document.getElementById('view'));
}());

View file

@ -17,7 +17,9 @@
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>. * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/ */
(function() { import Player from "./player";
"use strict"; "use strict";
var React = require("react"); var React = require("react");
@ -45,7 +47,7 @@
*/ */
var Terminal = React.createClass({ var Terminal = React.createClass({
propTypes: { propTypes: {
cols: React.PropTypes.number, // cols: React.PropTypes.number,
rows: React.PropTypes.number, rows: React.PropTypes.number,
channel: React.PropTypes.object.isRequired, channel: React.PropTypes.object.isRequired,
onTitleChanged: React.PropTypes.func onTitleChanged: React.PropTypes.func
@ -118,7 +120,7 @@
let style = { let style = {
'min-width': '300px', 'min-width': '300px',
'min-height': '100px', 'min-height': '100px',
} };
// ensure react never reuses this div by keying it with the terminal widget // 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} />; return <div ref="terminal" style={style} className="console-ct" key={this.state.terminal} />;
}, },
@ -187,5 +189,5 @@
} }
}); });
module.exports = { Terminal: Terminal }; // module.exports = { Terminal: Terminal };
}()); export class Terminal;

View file

@ -30,6 +30,7 @@ var info = {
"config": [ "config": [
"./config.jsx", "./config.jsx",
"./recordings.css", "./recordings.css",
"./table.css",
] ]
}, },
files: [ files: [
@ -38,6 +39,7 @@ var info = {
"player.jsx", "player.jsx",
"recordings.jsx", "recordings.jsx",
"recordings.css", "recordings.css",
"table.css",
"terminal.jsx", "terminal.jsx",
"manifest.json", "manifest.json",
"timer.css", "timer.css",