From 1abe64fe0c8d4612f4599ac8af7bbd50220ead73 Mon Sep 17 00:00:00 2001 From: Kyrylo Gliebov Date: Fri, 14 Sep 2018 18:34:33 +0200 Subject: [PATCH] Rebase and migration to full React instead of react-lite --- package.json | 21 +- src/config.jsx | 137 +- src/pkg/lib/cockpit-components-listing.jsx | 284 +-- src/player.jsx | 2051 ++++++++++---------- src/recordings.jsx | 1696 ++++++++-------- src/terminal.jsx | 330 ++-- webpack.config.js | 2 + 7 files changed, 2275 insertions(+), 2246 deletions(-) diff --git a/package.json b/package.json index 93b49da..cfb1fc9 100644 --- a/package.json +++ b/package.json @@ -42,27 +42,26 @@ "sass-loader": "^7.0.3", "sizzle": "^2.3.3", "stdio": "^0.2.7", - "webpack": "^4.17.1", + "webpack": "^4.19.0", "webpack-cli": "^3.1.0" }, "dependencies": { "@babel/polyfill": "^7.0.0", - "node-sass": "^4.9.0", - "react": "^16.4.2", - "react-dom": "^16.4.2" "bootstrap": "3.3.7", - "patternfly": "3.35.1", - "webpack": "^2.6.1", - "jquery": "3.3.1", - "moment": "2.22.2", - "mustache": "2.3.0", "bootstrap-datetime-picker": "2.4.4", "comment-json": "^1.1.3", - "term.js-cockpit": "0.0.10", "fs.extra": "^1.3.2", "fs.realpath": "^1.0.0", "ini": "^1.3.5", + "jquery": "3.3.1", + "moment": "2.22.2", + "mustache": "2.3.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" } } diff --git a/src/config.jsx b/src/config.jsx index ae02053..6367663 100644 --- a/src/config.jsx +++ b/src/config.jsx @@ -22,10 +22,11 @@ let cockpit = require("cockpit"); let React = require("react"); + let ReactDOM = require("react-dom"); let json = require('comment-json'); let ini = require('ini'); - let Config = class extends React.Component { + class Config extends React.Component { constructor(props) { super(props); this.handleInputChange = this.handleInputChange.bind(this); @@ -38,7 +39,7 @@ config: null, file_error: null, submitting: "none", - } + }; } handleInputChange(e) { @@ -59,10 +60,8 @@ 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) => { @@ -72,20 +71,16 @@ } 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); }; @@ -106,8 +101,6 @@ // host: string }); - console.log(this.file); - let promise = this.file.read(); promise.done((data) => { @@ -253,10 +246,9 @@ -
- +
@@ -274,9 +266,9 @@ ); } } - }; + } - let SssdConfig = class extends React.Component { + class SssdConfig extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); @@ -284,27 +276,24 @@ this.setConfig = this.setConfig.bind(this); this.file = null; this.state = { - config: { - session_recording: { - scope: null, - users: null, - groups: null, - }, - }, + scope: "", + users: "", + groups: "", + submitting: "none", }; } - handleInputChange(e){ + 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.session_recording[name] = value; - - this.forceUpdate(); + const state = {}; + state[name] = value; + this.setState(state); } setConfig(data) { - this.setState({config: data}); + const config = {...data['session_recording']}; + this.setState(config); } componentDidMount() { @@ -327,14 +316,21 @@ }); } - handleSubmit() { - this.file.replace(this.state.config).done( function() { - console.log('updated'); + handleSubmit(e) { + this.setState({submitting:"block"}); + 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) { - console.log(error); - }); - event.preventDefault(); + .fail(function(error) { + console.log(error); + }); + e.preventDefault(); } render() { @@ -342,49 +338,48 @@
- - - + - - - - - - - - + + + + - - - - - + + + + + + + + +
- + -
- -
- None + + + +
+ -
- - - -
+ +
+ + + +
); } - }; + } - React.render(, document.getElementById('sr_config')); - React.render(, document.getElementById('sssd_config')); + ReactDOM.render(, document.getElementById('sr_config')); + ReactDOM.render(, document.getElementById('sssd_config')); }()); diff --git a/src/pkg/lib/cockpit-components-listing.jsx b/src/pkg/lib/cockpit-components-listing.jsx index fe69f07..15cc683 100644 --- a/src/pkg/lib/cockpit-components-listing.jsx +++ b/src/pkg/lib/cockpit-components-listing.jsx @@ -17,11 +17,9 @@ * along with Cockpit; If not, see . */ -"use strict"; - -var React = require('react'); - -require('./listing.less'); +import PropTypes from 'prop-types'; +import React from 'react'; +import './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 @@ -47,55 +45,43 @@ require('./listing.less'); * 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 { +export class ListingRow extends React.Component { + constructor(props) { + super(props); + this.state = { 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) { + this.handleNavigateClick = this.handleNavigateClick.bind(this); + 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 if (!e || e.button !== 0) return; this.props.navigateToItem(); - }, - handleExpandClick: function(e) { + } + + handleExpandClick(e) { // only consider primary mouse button if (!e || e.button !== 0) 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 }); - var loadedTabs = {}; + let loadedTabs = {}; // unload all tabs if not expanded if (willBeExpanded) { // see if we should preload some tabs - var tabIdx; - var tabPresence; + let tabIdx; + let tabPresence; for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) { if ('presence' in this.props.tabRenderers[tabIdx]) tabPresence = this.props.tabRenderers[tabIdx].presence; @@ -115,13 +101,14 @@ var ListingRow = React.createClass({ e.stopPropagation(); e.preventDefault(); - }, - handleSelectClick: function(e) { + } + + handleSelectClick(e) { // only consider primary mouse button if (!e || e.button !== 0) return; - var selected = !this.state.selected; + let selected = !this.state.selected; this.setState({ selected: selected }); if (this.props.selectChanged) @@ -129,14 +116,15 @@ var ListingRow = React.createClass({ e.stopPropagation(); e.preventDefault(); - }, - handleTabClick: function(tabIdx, e) { + } + + handleTabClick(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; + let prevTab = this.state.activeTab; + let prevTabPresence = 'default'; + let loadedTabs = this.state.loadedTabs; if (prevTab !== tabIdx) { // see if we need to unload the previous tab if ('presence' in this.props.tabRenderers[prevTab]) @@ -151,41 +139,42 @@ var ListingRow = React.createClass({ } 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) { + 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)) - return ({itm}); + return ({itm}); else if ('header' in itm && itm.header) - return ({itm.name}); + return ({itm.name}); else if ('tight' in itm && itm.tight) - return ({itm.name || itm.element}); + return ({itm.name || itm.element}); else - return ({itm.name}); + return ({itm.name}); }); - var allowExpand = (this.props.tabRenderers.length > 0); - var expandToggle; + let allowExpand = (this.props.tabRenderers.length > 0); + let expandToggle; if (allowExpand) { - expandToggle = + expandToggle = ; } else { - expandToggle = ; + expandToggle = ; } - var listingItemClasses = ["listing-ct-item"]; + let 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; + let allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined); + let clickHandler; if (allowSelect) { clickHandler = this.handleSelectClick; if (this.state.selected) @@ -197,7 +186,7 @@ var ListingRow = React.createClass({ clickHandler = this.handleExpandClick; } - var listingItem = ( + let listingItem = ( @@ -207,18 +196,18 @@ var ListingRow = React.createClass({ ); if (this.state.expanded) { - var links = this.props.tabRenderers.map(function(itm, idx) { + let links = this.props.tabRenderers.map((itm, idx) => { return (
  • {itm.name}
  • ); }); - var tabs = []; - var tabIdx; - var Renderer; - var rendererData; - var row; + let tabs = []; + let tabIdx; + let Renderer; + let rendererData; + let row; for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) { Renderer = this.props.tabRenderers[tabIdx].renderer; rendererData = this.props.tabRenderers[tabIdx].data; @@ -231,7 +220,7 @@ var ListingRow = React.createClass({ tabs.push(); } - var listingDetail; + let listingDetail; if ('listingDetail' in this.props) { listingDetail = ( @@ -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 * https://www.patternfly.org/list-view/ * Properties: @@ -281,80 +288,77 @@ var ListingRow = React.createClass({ * 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 = {this.props.emptyCaption}; - } 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; +export const Listing = (props) => { + let bodyClasses = ["listing", "listing-ct"]; + if (props.fullWidth) + bodyClasses.push("listing-ct-wide"); + let headerClasses; + let headerRow; + let selectableRows; + if (!props.children || props.children.length === 0) { + headerClasses = "listing-ct-empty"; + headerRow = {props.emptyCaption}; + } else if (props.columnTitles.length) { + // check if any of the children are selectable + selectableRows = false; + 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 + props.children.forEach(function(r) { + if (r.props.selected === undefined) + r.props.selected = false; }); - - 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 = ( - - - { this.props.columnTitles.map(function (title, index) { - var clickHandler = null; - if (self.props.columnTitleClick) - clickHandler = function() { self.props.columnTitleClick(index) }; - return {title}; - }) } - - ); - } else { - headerRow = } - var caption; - if (this.props.title || (this.props.actions && this.props.actions.length > 0)) - caption = {this.props.title}{this.props.actions}; - return ( - - {caption} - - {headerRow} - - {this.props.children} -
    + headerRow = ( + + + { props.columnTitles.map((title, index) => { + let clickHandler = null; + if (props.columnTitleClick) + clickHandler = function() { props.columnTitleClick(index) }; + return {title}; + }) } + ); - }, -}); + } else { + headerRow = ; + } + let caption; + if (props.title || (props.actions && props.actions.length > 0)) + caption = {props.title}{props.actions}; -module.exports = { - ListingRow: ListingRow, - Listing: Listing, + return ( + + {caption} + + {headerRow} + + {props.children} +
    + ); +}; + +Listing.defaultProps = { + title: '', + fullWidth: true, + 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) }; diff --git a/src/player.jsx b/src/player.jsx index 53c75c3..d04cb5e 100644 --- a/src/player.jsx +++ b/src/player.jsx @@ -1,1132 +1,1121 @@ /* - * 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 . +* 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 . +*/ +"use strict"; +import React from 'react'; +let cockpit = require("cockpit"); +let _ = cockpit.gettext; +let Term = require("term.js-cockpit"); +let Journal = require("journal"); +let $ = require("jquery"); +require("console.css"); + +/* + * Get an object field, verifying its presence and type. */ - -(function() { - "use strict"; - - let cockpit = require("cockpit"); - let _ = cockpit.gettext; - let React = require("react"); - let Term = require("term.js-cockpit"); - let Journal = require("journal"); - let $ = require("jquery"); - - require("console.css"); - - /* - * Get an object field, verifying its presence and type. - */ - let getValidField = function (object, field, type) { - let value; - if (!(field in object)) { - throw Error("\"" + field + "\" field is missing"); - } - value = object[field]; - if (typeof (value) != typeof (type)) { - throw Error("invalid \"" + field + "\" field type: " + typeof (value)); - } - return value; +let getValidField = function (object, field, type) { + let value; + if (!(field in object)) { + throw Error("\"" + field + "\" field is missing"); } + value = object[field]; + if (typeof (value) != typeof (type)) { + throw Error("invalid \"" + field + "\" field type: " + typeof (value)); + } + return value; +}; - let scrollToBottom = function(id) { - const el = document.getElementById(id); - if (el) { - el.scrollTop = el.scrollHeight; - } +let scrollToBottom = function(id) { + const el = document.getElementById(id); + if (el) { + el.scrollTop = el.scrollHeight; + } +}; + +/* + * An auto-loading buffer of recording's packets. + */ +let PacketBuffer = class { + /* + * Initialize a buffer. + */ + constructor(matchList) { + this.handleError = this.handleError.bind(this); + this.handleStream = this.handleStream.bind(this); + this.handleDone = this.handleDone.bind(this); + /* RegExp used to parse message's timing field */ + this.timingRE = new RegExp( + /* Delay (1) */ + "\\+(\\d+)|" + + /* Text input (2) */ + "<(\\d+)|" + + /* Binary input (3, 4) */ + "\\[(\\d+)/(\\d+)|" + + /* Text output (5) */ + ">(\\d+)|" + + /* Binary output (6, 7) */ + "\\](\\d+)/(\\d+)|" + + /* Window (8, 9) */ + "=(\\d+)x(\\d+)|" + + /* End of string */ + "$", + /* Continue after the last match only */ + /* FIXME Support likely sparse */ + "y" + ); + /* List of matches to apply when loading the buffer from Journal */ + this.matchList = matchList; + /* + * An array of two-element arrays (tuples) each containing a + * packet index and a deferred object. The list is kept sorted to + * have tuples with lower packet indices first. Once the buffer + * receives a packet at the specified index, the matching tuple is + * removed from the list, and its deferred object is resolved. + * This is used to keep users informed about packets arriving. + */ + this.idxDfdList = []; + /* Last seen message ID */ + this.id = 0; + /* Last seen time position */ + this.pos = 0; + /* Last seen window width */ + this.width = null; + /* Last seen window height */ + this.height = null; + /* List of packets read */ + this.pktList = []; + /* Error which stopped the loading */ + this.error = null; + /* The journalctl reading the recording */ + this.journalctl = Journal.journalctl( + this.matchList, + {count: "all", follow: false}); + this.journalctl.fail(this.handleError); + this.journalctl.stream(this.handleStream); + this.journalctl.done(this.handleDone); + /* + * Last seen cursor of the first, non-follow, journalctl run. + * Null if no entry was received yet, or the second run has + * skipped the entry received last by the first run. + */ + this.cursor = null; + /* True if the first, non-follow, journalctl run has completed */ + this.done = false; } /* - * An auto-loading buffer of recording's packets. + * Return a promise which is resolved when a packet at a particular + * index is received by the buffer. The promise is rejected with a + * non-null argument if an error occurs or has occurred previously. + * The promise is rejected with null, when the buffer is stopped. If + * the packet index is not specified, assume it's the next packet. */ - let PacketBuffer = class { - /* - * Initialize a buffer. - */ - constructor(matchList) { - this.handleError = this.handleError.bind(this); - this.handleStream = this.handleStream.bind(this); - this.handleDone = this.handleDone.bind(this); - /* RegExp used to parse message's timing field */ - this.timingRE = new RegExp( - /* Delay (1) */ - "\\+(\\d+)|" + - /* Text input (2) */ - "<(\\d+)|" + - /* Binary input (3, 4) */ - "\\[(\\d+)/(\\d+)|" + - /* Text output (5) */ - ">(\\d+)|" + - /* Binary output (6, 7) */ - "\\](\\d+)/(\\d+)|" + - /* Window (8, 9) */ - "=(\\d+)x(\\d+)|" + - /* End of string */ - "$", - /* Continue after the last match only */ - /* FIXME Support likely sparse */ - "y" - ); - /* List of matches to apply when loading the buffer from Journal */ - this.matchList = matchList; - /* - * An array of two-element arrays (tuples) each containing a - * packet index and a deferred object. The list is kept sorted to - * have tuples with lower packet indices first. Once the buffer - * receives a packet at the specified index, the matching tuple is - * removed from the list, and its deferred object is resolved. - * This is used to keep users informed about packets arriving. - */ - this.idxDfdList = []; - /* Last seen message ID */ - this.id = 0; - /* Last seen time position */ - this.pos = 0; - /* Last seen window width */ - this.width = null; - /* Last seen window height */ - this.height = null; - /* List of packets read */ - this.pktList = []; - /* Error which stopped the loading */ - this.error = null; - /* The journalctl reading the recording */ - this.journalctl = Journal.journalctl( - this.matchList, - {count: "all", follow: false}); - this.journalctl.fail(this.handleError); - this.journalctl.stream(this.handleStream); - this.journalctl.done(this.handleDone); - /* - * Last seen cursor of the first, non-follow, journalctl run. - * Null if no entry was received yet, or the second run has - * skipped the entry received last by the first run. - */ - this.cursor = null; - /* True if the first, non-follow, journalctl run has completed */ - this.done = false; + awaitPacket(idx) { + let i; + let idxDfd; + + /* If an error has occurred previously */ + if (this.error !== null) { + /* Reject immediately */ + return $.Deferred().reject(this.error) + .promise(); } - /* - * Return a promise which is resolved when a packet at a particular - * index is received by the buffer. The promise is rejected with a - * non-null argument if an error occurs or has occurred previously. - * The promise is rejected with null, when the buffer is stopped. If - * the packet index is not specified, assume it's the next packet. - */ - awaitPacket(idx) { - let i; - let idxDfd; + /* If the buffer was stopped */ + if (this.journalctl === null) { + return $.Deferred().reject(null) + .promise(); + } - /* If an error has occurred previously */ - if (this.error !== null) { - /* Reject immediately */ - return $.Deferred().reject(this.error) + /* If packet index is not specified */ + if (idx === undefined) { + /* Assume it's the next one */ + idx = this.pktList.length; + } else { + /* If it has already been received */ + if (idx < this.pktList.length) { + /* Return resolved promise */ + return $.Deferred().resolve() .promise(); } + } - /* If the buffer was stopped */ - if (this.journalctl === null) { - return $.Deferred().reject(null) - .promise(); + /* Try to find an existing, matching tuple */ + for (i = 0; i < this.idxDfdList.length; i++) { + idxDfd = this.idxDfdList[i]; + if (idxDfd[0] == idx) { + return idxDfd[1].promise(); + } else if (idxDfd[0] > idx) { + break; } + } - /* If packet index is not specified */ - if (idx === undefined) { - /* Assume it's the next one */ - idx = this.pktList.length; + /* Not found, create and insert a new tuple */ + idxDfd = [idx, $.Deferred()]; + this.idxDfdList.splice(i, 0, idxDfd); + + /* Return its promise */ + return idxDfd[1].promise(); + } + + /* + * Return true if the buffer was done loading everything logged to + * journal so far and is now waiting for and loading new entries. + * Return false if the buffer is loading existing entries so far. + */ + isDone() { + return this.done; + } + + /* + * Stop receiving the entries + */ + stop() { + if (this.journalctl === null) { + return; + } + /* Destroy journalctl */ + this.journalctl.stop(); + this.journalctl = null; + /* Notify everyone we stopped */ + for (let i = 0; i < this.idxDfdList.length; i++) { + this.idxDfdList[i][1].reject(null); + } + this.idxDfdList = []; + } + + /* + * Add a packet to the received packet list. + */ + addPacket(pkt) { + /* TODO Validate the packet */ + /* Add the packet */ + this.pktList.push(pkt); + /* Notify any matching listeners */ + while (this.idxDfdList.length > 0) { + let idxDfd = this.idxDfdList[0]; + if (idxDfd[0] < this.pktList.length) { + this.idxDfdList.shift(); + idxDfd[1].resolve(); } else { - /* If it has already been received */ - if (idx < this.pktList.length) { - /* Return resolved promise */ - return $.Deferred().resolve() - .promise(); - } + break; } - - /* Try to find an existing, matching tuple */ - for (i = 0; i < this.idxDfdList.length; i++) { - idxDfd = this.idxDfdList[i]; - if (idxDfd[0] == idx) { - return idxDfd[1].promise(); - } else if (idxDfd[0] > idx) { - break; - } - } - - /* Not found, create and insert a new tuple */ - idxDfd = [idx, $.Deferred()]; - this.idxDfdList.splice(i, 0, idxDfd); - - /* Return its promise */ - return idxDfd[1].promise(); } + } - /* - * Return true if the buffer was done loading everything logged to - * journal so far and is now waiting for and loading new entries. - * Return false if the buffer is loading existing entries so far. - */ - isDone() { - return this.done; - } - - /* - * Stop receiving the entries - */ - stop() { - if (this.journalctl === null) { - return; - } - /* Destroy journalctl */ + /* + * Handle an error. + */ + handleError(error) { + /* Remember the error */ + this.error = error; + /* Destroy journalctl, don't try to recover */ + if (this.journalctl !== null) { this.journalctl.stop(); this.journalctl = null; - /* Notify everyone we stopped */ - for (let i = 0; i < this.idxDfdList.length; i++) { - this.idxDfdList[i][1].reject(null); - } - this.idxDfdList = []; } + /* Notify everyone we had an error */ + for (let i = 0; i < this.idxDfdList.length; i++) { + this.idxDfdList[i][1].reject(error); + } + this.idxDfdList = []; + } - /* - * Add a packet to the received packet list. - */ - addPacket(pkt) { - /* TODO Validate the packet */ - /* Add the packet */ - this.pktList.push(pkt) - /* Notify any matching listeners */ - while (this.idxDfdList.length > 0) { - let idxDfd = this.idxDfdList[0]; - if (idxDfd[0] < this.pktList.length) { - this.idxDfdList.shift(); - idxDfd[1].resolve(); - } else { + /* + * Parse packets out of a tlog message data and add them to the buffer. + */ + parseMessageData(timing, in_txt, out_txt) { + let matches; + let in_txt_pos = 0; + let out_txt_pos = 0; + let t; + let x; + let y; + let s; + let io = []; + let is_output; + + /* While matching entries in timing */ + this.timingRE.lastIndex = 0; + for (;;) { + /* Match next timing entry */ + matches = this.timingRE.exec(timing); + if (matches === null) { + throw Error("invalid timing string"); + } else if (matches[0] == "") { + break; + } + + /* Switch on entry type character */ + switch (t = matches[0][0]) { + /* Delay */ + case "+": + x = parseInt(matches[1], 10); + if (x == 0) { break; } - } - } - - /* - * Handle an error. - */ - handleError(error) { - /* Remember the error */ - this.error = error; - /* Destroy journalctl, don't try to recover */ - if (this.journalctl !== null) { - this.journalctl.stop(); - this.journalctl = null; - } - /* Notify everyone we had an error */ - for (let i = 0; i < this.idxDfdList.length; i++) { - this.idxDfdList[i][1].reject(error); - } - this.idxDfdList = []; - } - - /* - * Parse packets out of a tlog message data and add them to the buffer. - */ - parseMessageData(timing, in_txt, out_txt) { - let matches; - let in_txt_pos = 0; - let out_txt_pos = 0; - let t; - let x; - let y; - let s; - let io = []; - let is_output; - - /* While matching entries in timing */ - this.timingRE.lastIndex = 0; - for (;;) { - /* Match next timing entry */ - matches = this.timingRE.exec(timing); - if (matches === null) { - throw Error("invalid timing string"); - } else if (matches[0] == "") { - break; - } - - /* Switch on entry type character */ - switch (t = matches[0][0]) { - /* Delay */ - case "+": - x = parseInt(matches[1], 10); - if (x == 0) { - break; - } - if (io.length > 0) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); - io = []; - } - this.pos += x; - break; - /* Text or binary input */ - case "<": - case "[": - x = parseInt(matches[(t == "<") ? 2 : 3], 10); - if (x == 0) { - break; - } - if (io.length > 0 && is_output) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); - io = []; - } - is_output = false; - /* Add (replacement) input characters */ - s = in_txt.slice(in_txt_pos, in_txt_pos += x); - if (s.length != x) { - throw Error("timing entry out of input bounds"); - } - io.push(s); - break; - /* Text or binary output */ - case ">": - case "]": - x = parseInt(matches[(t == ">") ? 5 : 6], 10); - if (x == 0) { - break; - } - if (io.length > 0 && !is_output) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); - io = []; - } - is_output = true; - /* Add (replacement) output characters */ - s = out_txt.slice(out_txt_pos, out_txt_pos += x); - if (s.length != x) { - throw Error("timing entry out of output bounds"); - } - io.push(s); - break; - /* Window */ - case "=": - x = parseInt(matches[8], 10); - y = parseInt(matches[9], 10); - if (x == this.width && y == this.height) { - break; - } - if (io.length > 0) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); - io = []; - } + if (io.length > 0) { this.addPacket({pos: this.pos, - is_io: false, - width: x, - height: y}); - this.width = x; - this.height = y; + is_io: true, + is_output: is_output, + io: io.join()}); + io = []; + } + this.pos += x; + break; + /* Text or binary input */ + case "<": + case "[": + x = parseInt(matches[(t == "<") ? 2 : 3], 10); + if (x == 0) { break; } - } - - if (in_txt_pos < in_txt.length) { - throw Error("extra input present"); - } - if (out_txt_pos < out_txt.length) { - throw Error("extra output present"); - } - - if (io.length > 0) { + if (io.length > 0 && is_output) { + this.addPacket({pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join()}); + io = []; + } + is_output = false; + /* Add (replacement) input characters */ + s = in_txt.slice(in_txt_pos, in_txt_pos += x); + if (s.length != x) { + throw Error("timing entry out of input bounds"); + } + io.push(s); + break; + /* Text or binary output */ + case ">": + case "]": + x = parseInt(matches[(t == ">") ? 5 : 6], 10); + if (x == 0) { + break; + } + if (io.length > 0 && !is_output) { + this.addPacket({pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join()}); + io = []; + } + is_output = true; + /* Add (replacement) output characters */ + s = out_txt.slice(out_txt_pos, out_txt_pos += x); + if (s.length != x) { + throw Error("timing entry out of output bounds"); + } + io.push(s); + break; + /* Window */ + case "=": + x = parseInt(matches[8], 10); + y = parseInt(matches[9], 10); + if (x == this.width && y == this.height) { + break; + } + if (io.length > 0) { + this.addPacket({pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join()}); + io = []; + } this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); + is_io: false, + width: x, + height: y}); + this.width = x; + this.height = y; + break; } } - /* - * Parse packets out of a tlog message and add them to the buffer. - */ - parseMessage(message) { - let matches; - let ver; - let id; - let pos; - - const number = Number(); - const string = String(); - - /* Check version */ - ver = getValidField(message, "ver", string); - matches = ver.match("^(\\d+)\\.(\\d+)$"); - if (matches === null || matches[1] > 2) { - throw Error("\"ver\" field has invalid value: " + ver); - } - - /* TODO Perhaps check host, rec, user, term, and session fields */ - - /* Extract message ID */ - id = getValidField(message, "id", number); - if (id <= this.id) { - throw Error("out of order \"id\" field value: " + id); - } - - /* Extract message time position */ - pos = getValidField(message, "pos", number); - if (pos < this.message_pos) { - throw Error("out of order \"pos\" field value: " + pos); - } - - /* Update last received message ID and time position */ - this.id = id; - this.pos = pos; - - /* Parse message data */ - this.parseMessageData( - getValidField(message, "timing", string), - getValidField(message, "in_txt", string), - getValidField(message, "out_txt", string)); + if (in_txt_pos < in_txt.length) { + throw Error("extra input present"); + } + if (out_txt_pos < out_txt.length) { + throw Error("extra output present"); } - /* - * Handle journalctl "stream" event. - */ - handleStream(entryList) { - let i; - let e; - for (i = 0; i < entryList.length; i++) { - e = entryList[i]; - /* If this is the second, "follow", run */ - if (this.done) { - /* Skip the last entry we added on the first run */ - if (this.cursor !== null) { - this.cursor = null; - continue; - } - } else { - if (!('__CURSOR' in e)) { - this.handleError("No cursor in a Journal entry"); - } - this.cursor = e['__CURSOR']; + if (io.length > 0) { + this.addPacket({pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join()}); + } + } + + /* + * Parse packets out of a tlog message and add them to the buffer. + */ + parseMessage(message) { + let matches; + let ver; + let id; + let pos; + + const number = Number(); + const string = String(); + + /* Check version */ + ver = getValidField(message, "ver", string); + matches = ver.match("^(\\d+)\\.(\\d+)$"); + if (matches === null || matches[1] > 2) { + throw Error("\"ver\" field has invalid value: " + ver); + } + + /* TODO Perhaps check host, rec, user, term, and session fields */ + + /* Extract message ID */ + id = getValidField(message, "id", number); + if (id <= this.id) { + throw Error("out of order \"id\" field value: " + id); + } + + /* Extract message time position */ + pos = getValidField(message, "pos", number); + if (pos < this.message_pos) { + throw Error("out of order \"pos\" field value: " + pos); + } + + /* Update last received message ID and time position */ + this.id = id; + this.pos = pos; + + /* Parse message data */ + this.parseMessageData( + getValidField(message, "timing", string), + getValidField(message, "in_txt", string), + getValidField(message, "out_txt", string)); + } + + /* + * Handle journalctl "stream" event. + */ + handleStream(entryList) { + let i; + let e; + for (i = 0; i < entryList.length; i++) { + e = entryList[i]; + /* If this is the second, "follow", run */ + if (this.done) { + /* Skip the last entry we added on the first run */ + if (this.cursor !== null) { + this.cursor = null; + continue; } - /* TODO Refer to entry number/cursor in errors */ - if (!('MESSAGE' in e)) { - this.handleError("No message in Journal entry"); - } - /* Parse the entry message */ - try { - this.parseMessage(JSON.parse(e['MESSAGE'])); - } catch (error) { - this.handleError(error); - return; + } else { + if (!('__CURSOR' in e)) { + this.handleError("No cursor in a Journal entry"); } + this.cursor = e['__CURSOR']; + } + /* TODO Refer to entry number/cursor in errors */ + if (!('MESSAGE' in e)) { + this.handleError("No message in Journal entry"); + } + /* Parse the entry message */ + try { + this.parseMessage(JSON.parse(e['MESSAGE'])); + } catch (error) { + this.handleError(error); + return; } } + } - /* - * Handle journalctl "done" event. - */ - handleDone() { - this.done = true; - this.journalctl.stop(); - /* Continue with the "following" run */ - this.journalctl = Journal.journalctl( - this.matchList, - {cursor: this.cursor, - follow: true, count: "all"}); - this.journalctl.fail(this.handleError); - this.journalctl.stream(this.handleStream); - /* NOTE: no "done" handler on purpose */ + /* + * Handle journalctl "done" event. + */ + handleDone() { + this.done = true; + this.journalctl.stop(); + /* Continue with the "following" run */ + this.journalctl = Journal.journalctl( + this.matchList, + {cursor: this.cursor, + follow: true, count: "all"}); + this.journalctl.fail(this.handleError); + this.journalctl.stream(this.handleStream); + /* NOTE: no "done" handler on purpose */ + } +}; + +let ProgressBar = class extends React.Component { + constructor(props) { + super(props); + this.jumpTo = this.jumpTo.bind(this); + } + + jumpTo(e) { + if (this.props.fastForwardFunc) { + let percent = parseInt((e.nativeEvent.offsetX * 100) / e.currentTarget.clientWidth); + let ts = parseInt((this.props.length * percent) / 100); + this.props.fastForwardFunc(ts); } - }; + } - let ProgressBar = class extends React.Component { - constructor(props) { - super(props); - this.jumpTo = this.jumpTo.bind(this); - } + render() { + let progress = { + "width": parseInt((this.props.mark * 100) / this.props.length) + "%" + }; - jumpTo(e) { - if (this.props.fastForwardFunc) { - let percent = parseInt((e.offsetX * 100) / e.currentTarget.clientWidth); - let ts = parseInt((this.props.length * percent) / 100); - this.props.fastForwardFunc(ts); - } - } + return ( +
    +
    +
    + ); + } +}; - render() { - let progress = { - "width": parseInt((this.props.mark * 100) / this.props.length) + "%" - }; - - return ( -
    -
    +let InputPlayer = class extends React.Component { + render() { + return ( +
    +
    + Input
    - ); - } - }; - - let InputPlayer = class extends React.Component { - constructor(props) { - super(props); - } - - render() { - return( -
    -
    - Input -
    -
    - -
    +
    +