From a20e3c5a81141e7377598134875c08f2023ef4f6 Mon Sep 17 00:00:00 2001 From: Kyrylo Gliebov Date: Mon, 2 Jul 2018 18:03:59 +0200 Subject: [PATCH] Session recording module for Cockpit initial commit --- ...ec.in => cockpit-session-recording.spec.in | 10 +- ...pit-project.session-recording.metainfo.xml | 6 +- package.json | 19 +- src/app.scss | 3 - src/config.html | 54 + src/config.jsx | 279 +++++ src/index.html | 29 +- src/manifest.json | 11 +- src/manifest.json.in | 15 + src/pkg/lib/cockpit-components-listing.jsx | 360 ++++++ src/pkg/lib/console.css | 59 + src/pkg/lib/journal.css | 134 ++ src/pkg/lib/journal.js | 584 +++++++++ src/pkg/lib/journal_day_header.mustache | 1 + src/pkg/lib/journal_line.mustache | 19 + src/pkg/lib/journal_reboot.mustache | 5 + src/pkg/lib/listing.less | 502 ++++++++ src/pkg/lib/page.css | 235 ++++ src/pkg/lib/table.css | 146 +++ src/pkg/lib/term.css | 522 ++++++++ src/pkg/lib/variables.less | 22 + src/player.jsx | 1080 +++++++++++++++++ src/recordings.css | 368 ++++++ src/recordings.jsx | 748 ++++++++++++ src/terminal.jsx | 191 +++ src/timer.css | 163 +++ webpack.config.js | 31 +- 27 files changed, 5564 insertions(+), 32 deletions(-) rename cockpit-starter-kit.spec.in => cockpit-session-recording.spec.in (54%) rename org.cockpit-project.starter-kit.metainfo.xml => org.cockpit-project.session-recording.metainfo.xml (63%) delete mode 100644 src/app.scss create mode 100644 src/config.html create mode 100644 src/config.jsx create mode 100644 src/manifest.json.in create mode 100644 src/pkg/lib/cockpit-components-listing.jsx create mode 100644 src/pkg/lib/console.css create mode 100644 src/pkg/lib/journal.css create mode 100644 src/pkg/lib/journal.js create mode 100644 src/pkg/lib/journal_day_header.mustache create mode 100644 src/pkg/lib/journal_line.mustache create mode 100644 src/pkg/lib/journal_reboot.mustache create mode 100644 src/pkg/lib/listing.less create mode 100644 src/pkg/lib/page.css create mode 100644 src/pkg/lib/table.css create mode 100644 src/pkg/lib/term.css create mode 100644 src/pkg/lib/variables.less create mode 100644 src/player.jsx create mode 100644 src/recordings.css create mode 100644 src/recordings.jsx create mode 100644 src/terminal.jsx create mode 100644 src/timer.css diff --git a/cockpit-starter-kit.spec.in b/cockpit-session-recording.spec.in similarity index 54% rename from cockpit-starter-kit.spec.in rename to cockpit-session-recording.spec.in index 53e8b70..0359706 100644 --- a/cockpit-starter-kit.spec.in +++ b/cockpit-session-recording.spec.in @@ -1,19 +1,19 @@ -Name: cockpit-starter-kit +Name: cockpit-session-recording Version: @VERSION@ Release: 1%{?dist} -Summary: Cockpit Starter Kit Example Module +Summary: Cockpit Session Recording License: LGPLv2+ -Source: cockpit-starter-kit-%{version}.tar.gz +Source: cockpit-session-recording-%{version}.tar.gz BuildArch: noarch %define debug_package %{nil} %description -Cockpit Starter Kit Example Module +Cockpit Session Recording %prep -%setup -n cockpit-starter-kit +%setup -n cockpit-session-recording %install %make_install diff --git a/org.cockpit-project.starter-kit.metainfo.xml b/org.cockpit-project.session-recording.metainfo.xml similarity index 63% rename from org.cockpit-project.starter-kit.metainfo.xml rename to org.cockpit-project.session-recording.metainfo.xml index ad720d8..1cd1106 100644 --- a/org.cockpit-project.starter-kit.metainfo.xml +++ b/org.cockpit-project.session-recording.metainfo.xml @@ -1,7 +1,7 @@ - org.cockpit-project.starter-kit + org.cockpit-project.session-recording CC0-1.0 - Starter Kit + Session Recording Scaffolding for a cockpit module. @@ -11,5 +11,5 @@

cockpit.desktop - cockpit-starter-kit + cockpit-session-recording
diff --git a/package.json b/package.json index ae2f5a8..fbb9665 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "starter-kit", + "name": "session-recording", "version": "0.1.0", "description": "Scaffolding for a cockpit module", "main": "index.js", @@ -34,6 +34,10 @@ "extract-text-webpack-plugin": "^4.0.0-beta.0", "htmlparser": "^1.7.7", "jed": "^1.1.1", + "jshint": "~2.9.1", + "jshint-loader": "~0.8.3", + "less": "~3.0.1", + "less-loader": "~4.0.6", "po2json": "^0.4.5", "sass-loader": "^7.0.3", "sizzle": "^2.3.3", @@ -46,5 +50,18 @@ "node-sass": "^4.9.0", "react": "^16.4.2", "react-dom": "^16.4.2" + "bootstrap": "3.3.7", + "patternfly": "3.35.1", + "webpack": "^2.6.1", + "jquery": "3.3.1", + "moment": "2.22.2", + "mustache": "2.3.0", + "bootstrap-datetime-picker": "2.4.4", + "comment-json": "^1.1.3", + "term.js-cockpit": "0.0.10", + "fs.extra": "^1.3.2", + "fs.realpath": "^1.0.0", + "node-sass": "^4.9.0", + "raw-loader": "^0.5.1" } } diff --git a/src/app.scss b/src/app.scss deleted file mode 100644 index 87b46f4..0000000 --- a/src/app.scss +++ /dev/null @@ -1,3 +0,0 @@ -p { - font-weight: bold; -} diff --git a/src/config.html b/src/config.html new file mode 100644 index 0000000..757a1ee --- /dev/null +++ b/src/config.html @@ -0,0 +1,54 @@ + + + + + + Journal + + + + + + + + +
+
+
+
+ +
+
+
+
+
+
Configuration
+
+
+
+
+
+ + + + diff --git a/src/config.jsx b/src/config.jsx new file mode 100644 index 0000000..0277128 --- /dev/null +++ b/src/config.jsx @@ -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 . + */ + +(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 ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ + +
+
+ ); + } else { + return ( +
+ +

There is no configuration file of tlog present in your system.

+

Please, check the /etc/tlog/tlog-rec-session.conf or if tlog is installed.

+

{this.state.file_error}

+
+ ); + } + } + } + + React.render(, document.getElementById('view')); +}()); diff --git a/src/index.html b/src/index.html index 5182d9b..e23d550 100644 --- a/src/index.html +++ b/src/index.html @@ -1,5 +1,7 @@ - + + - Cockpit Starter Kit - - - - - - - - - - + Journal + + + + + + -
+
+ + diff --git a/src/manifest.json b/src/manifest.json index 3e2454a..2fe650b 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,12 +1,15 @@ { - "version": "0.1", + "version": "163.x", + "name": "session_recording", + "requires": { - "cockpit": "137" + "cockpit": "122" }, - "tools": { + "menu": { "index": { - "label": "Starter Kit" + "label": "Session Recording", + "order": 100 } } } diff --git a/src/manifest.json.in b/src/manifest.json.in new file mode 100644 index 0000000..bab1160 --- /dev/null +++ b/src/manifest.json.in @@ -0,0 +1,15 @@ +{ + "version": "@VERSION@", + "name": "session_recording", + + "requires": { + "cockpit": "122" + }, + + "menu": { + "index": { + "label": "Session Recording", + "order": 100 + } + } +} diff --git a/src/pkg/lib/cockpit-components-listing.jsx b/src/pkg/lib/cockpit-components-listing.jsx new file mode 100644 index 0000000..fe69f07 --- /dev/null +++ b/src/pkg/lib/cockpit-components-listing.jsx @@ -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 . + */ + +"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 + * 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, is used for the entries, otherwise + * 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 ({itm}); + else if ('header' in itm && itm.header) + return ({itm.name}); + else if ('tight' in itm && itm.tight) + return ({itm.name || itm.element}); + else + return ({itm.name}); + }); + + var allowExpand = (this.props.tabRenderers.length > 0); + var expandToggle; + if (allowExpand) { + expandToggle = + + ; + } else { + expandToggle = ; + } + + 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 = ( + + {expandToggle} + {headerEntries} + + ); + + if (this.state.expanded) { + var links = this.props.tabRenderers.map(function(itm, idx) { + return ( +
  • + {itm.name} +
  • + ); + }); + 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 =