diff --git a/src/app.jsx b/src/app.jsx index 55f1c88..b94a4d2 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -20,28 +20,24 @@ import cockpit from 'cockpit'; import React from 'react'; import './app.scss'; +import View from "./recordings.jsx"; const _ = cockpit.gettext; export class Application extends React.Component { constructor() { super(); - this.state = { 'hostname': _("Unknown") }; + this.state = { hostname: _("Unknown") }; cockpit.file('/etc/hostname').read() .done((content) => { - this.setState({ 'hostname': content.trim() }); + this.setState({ hostname: content.trim() }); }); } render() { return ( -
-

Starter Kit

-

- { cockpit.format(_("Running on $0"), this.state.hostname) } -

-
+ ); } } diff --git a/src/app.scss b/src/app.scss new file mode 100644 index 0000000..87b46f4 --- /dev/null +++ b/src/app.scss @@ -0,0 +1,3 @@ +p { + font-weight: bold; +} diff --git a/src/config.html b/src/config.html deleted file mode 100644 index 1184fbd..0000000 --- a/src/config.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Journal - - - - - - - - - -
- - - - diff --git a/src/config.jsx b/src/config.jsx index b1e9d4a..3371819 100644 --- a/src/config.jsx +++ b/src/config.jsx @@ -18,16 +18,38 @@ */ "use strict"; -let cockpit = require("cockpit"); -let React = require("react"); -let ReactDOM = require("react-dom"); -let json = require('comment-json'); -let ini = require('ini'); +import React from "react"; +import { + Button, + Form, + FormGroup, + FormSelect, + FormSelectOption, + TextInput, + ActionGroup, + Spinner, + Card, + CardTitle, + CardBody, + Checkbox, + Bullseye, + EmptyState, + EmptyStateIcon, + Title, + EmptyStateBody, + EmptyStateVariant +} from "@patternfly/react-core"; +import { AngleLeftIcon, ExclamationCircleIcon } from "@patternfly/react-icons"; +import { global_danger_color_200 } from "@patternfly/react-tokens"; -class Config extends React.Component { +const json = require('comment-json'); +const ini = require('ini'); +const cockpit = require('cockpit'); +const _ = cockpit.gettext; + +class GeneralConfig 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.fileReadFailed = this.fileReadFailed.bind(this); @@ -37,7 +59,7 @@ class Config extends React.Component { this.state = { config_loaded: false, file_error: false, - submitting: "none", + submitting: false, shell: "", notice: "", latency: "", @@ -57,17 +79,9 @@ class Config extends React.Component { }; } - handleInputChange(e) { - const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; - const name = e.target.name; - const state = {}; - state[name] = value; - this.setState(state); - } - handleSubmit(event) { - this.setState({submitting:"block"}); - let config = { + this.setState({ submitting: true }); + const config = { shell: this.state.shell, notice: this.state.notice, latency: parseInt(this.state.latency), @@ -96,7 +110,7 @@ class Config extends React.Component { writer: this.state.writer }; this.file.replace(config).done(() => { - this.setState({submitting:"none"}); + this.setState({ submitting: false }); }) .fail((error) => { console.log(error); @@ -111,12 +125,12 @@ class Config extends React.Component { var toReturn = {}; for (var i in ob) { - if (!ob.hasOwnProperty(i)) continue; + if (!Object.prototype.hasOwnProperty.call(ob, i)) continue; if ((typeof ob[i]) == 'object') { var flatObject = flattenObject(ob[i]); for (var x in flatObject) { - if (!flatObject.hasOwnProperty(x)) continue; + if (!Object.prototype.hasOwnProperty.call(flatObject, x)) continue; toReturn[i + '_' + x] = flatObject[x]; } @@ -126,13 +140,13 @@ class Config extends React.Component { } return toReturn; }; - let state = flattenObject(data); + const state = flattenObject(data); state.config_loaded = true; this.setState(state); } getConfig() { - let proc = cockpit.spawn(["tlog-rec-session", "--configuration"]); + const proc = cockpit.spawn(["tlog-rec-session", "--configuration"]); proc.stream((data) => { this.setConfig(json.parse(data, null, true)); @@ -146,15 +160,15 @@ class Config extends React.Component { } readConfig() { - let parseFunc = function(data) { + const parseFunc = function(data) { return json.parse(data, null, true); }; - let stringifyFunc = function(data) { + const stringifyFunc = function(data) { return json.stringify(data, null, true); }; // needed for cockpit.file usage - let syntax_object = { + const syntax_object = { parse: parseFunc, stringify: stringifyFunc, }; @@ -163,22 +177,11 @@ class Config extends React.Component { syntax: syntax_object, superuser: true, }); - /* - let promise = this.file.read(); - - promise.done((data) => { - if (data === null) { - this.fileReadFailed(); - } - }).fail((data) => { - this.fileReadFailed(data); - }); - */ } fileReadFailed(reason) { console.log(reason); - this.setState({file_error: reason}); + this.setState({ file_error: reason }); } componentDidMount() { @@ -187,160 +190,201 @@ class Config extends React.Component { } render() { - if (this.state.config_loaded === false && this.state.file_error === false) { - return ( -
Loading
- ); - } else if (this.state.config_loaded === true && this.state.file_error === false) { - return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + const form = + (this.state.config_loaded === false && this.state.file_error === false) + ? + : (this.state.config_loaded === true && this.state.file_error === false) + ? ( + + + this.setState({ shell })} /> + + + this.setState({ notice })} /> + + + this.setState({ latency })} /> + + + this.setState({ payload })} /> + + + this.setState({ log_input })} + label={_("User's Input")} /> + this.setState({ log_output })} + label={_("User's Output")} /> + this.setState({ log_window })} + label={_("Window Resize")} /> + + + this.setState({ limit_rate })} /> + + + this.setState({ limit_burst })} /> + + + this.setState({ limit_action })}> + {[ + { value: "", label: "" }, + { value: "pass", label: _("Pass") }, + { value: "delay", label: _("Delay") }, + { value: "drop", label: _("Drop") } + ].map((option, index) => + + )} + + + + this.setState({ file_path })} /> + + + + this.setState({ syslog_facility })} /> + + + + this.setState({ syslog_priority })}> + {[ + { value: "", label: "" }, + { value: "info", label: _("Info") }, + ].map((option, index) => + + )} + + + + + this.setState({ journal_priority })}> + {[ + { value: "", label: "" }, + { value: "info", label: _("Info") }, + ].map((option, index) => + + )} + + + + + this.setState({ journal_augment })} + label={_("Augment")} /> + + + + this.setState({ writer })}> + {[ + { value: "", label: "" }, + { value: "journal", label: _("Journal") }, + { value: "syslog", label: _("Syslog") }, + { value: "file", label: _("File") }, + ].map((option, index) => + + )} + + + + + {this.state.submitting === true && } + + + ) + : ( + + + + + {_("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} + + + + ); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- - -
- - - Saving... -
- - ); - } 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}

-
- ); - } + return ( + + General Config + {form} + + ); } } @@ -348,7 +392,6 @@ class SssdConfig extends React.Component { constructor(props) { super(props); this.handleSubmit = this.handleSubmit.bind(this); - this.handleInputChange = this.handleInputChange.bind(this); this.setConfig = this.setConfig.bind(this); this.confSave = this.confSave.bind(this); this.file = null; @@ -356,16 +399,20 @@ class SssdConfig extends React.Component { scope: "", users: "", groups: "", - submitting: "none", + submitting: false, }; } confSave(obj) { - this.setState({submitting:"block"}); + this.setState({ submitting: true }); this.file.replace(obj).done(() => { - cockpit.spawn(["chmod", "600", "/etc/sssd/conf.d/sssd-session-recording.conf"], { "superuser": "require" }).done(() => { - cockpit.spawn(["systemctl", "restart", "sssd"], { "superuser": "require" }).done(() => { - this.setState({submitting:"none"}); + cockpit.spawn( + ["chmod", "600", "/etc/sssd/conf.d/sssd-session-recording.conf"], + { superuser: "require" }).done(() => { + cockpit.spawn( + ["systemctl", "restart", "sssd"], + { superuser: "require" }).done(() => { + this.setState({ submitting: false }); }) .fail((data) => console.log(data)); }) @@ -373,14 +420,6 @@ class SssdConfig extends React.Component { }); } - handleInputChange(e) { - const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; - const name = e.target.name; - const state = {}; - state[name] = value; - this.setState(state); - } - setConfig(data) { if (data === null) { const obj = {}; @@ -388,13 +427,13 @@ class SssdConfig extends React.Component { obj.session_recording.scope = "none"; this.confSave(obj); } else { - const config = {...data['session_recording']}; + const config = { ...data.session_recording }; this.setState(config); } } componentDidMount() { - let syntax_object = { + const syntax_object = { parse: ini.parse, stringify: ini.stringify }; @@ -404,7 +443,7 @@ class SssdConfig extends React.Component { superuser: true, }); - let promise = this.file.read(); + const promise = this.file.read(); promise.done(() => this.file.watch(this.setConfig)); @@ -424,95 +463,75 @@ class SssdConfig extends React.Component { } render() { + const form = ( +
+ + this.setState({ scope })}> + {[ + { value: "none", label: _("None") }, + { value: "some", label: _("Some") }, + { value: "all", label: _("All") } + ].map((option, index) => + + )} + + + {this.state.scope === "some" && + <> + + this.setState({ users })} + /> + + + this.setState({ groups })} + /> + + } + + + {this.state.submitting === true && } + +
+ ); + return ( -
- - - - - - - {this.state.scope === "some" && - - - - - } - {this.state.scope === "some" && - - - - - } - - - - - -
- -
- -
- -
- Saving... -
-
+ + SSSD Config + {form} + ); } } -class ConfigView extends React.Component { - render() { - const goBack = () => { - cockpit.jump(['session-recording']); - }; +export function Config () { + const goBack = () => { + cockpit.location.go("/"); + }; - return ( -
-
-
-
    -
  1. Session - Recording
  2. -
  3. Configuration
  4. -
-
-
-
-
-
-
General Configuration
-
- -
-
-
-
-
-
-
-
SSSD Configuration
-
- -
-
-
-
-
- ); - } + return ( + <> + + + + + ); } - -ReactDOM.render(, document.getElementById('view')); diff --git a/src/index.html b/src/index.html index 0e49141..cdf58a1 100644 --- a/src/index.html +++ b/src/index.html @@ -20,18 +20,20 @@ along with Cockpit; If not, see . - Journal - - - - - - + Session Recording + + + + + + + + + - -
- + +
diff --git a/src/index.es6 b/src/index.js similarity index 66% rename from src/index.es6 rename to src/index.js index cb3ca05..85fa0c4 100644 --- a/src/index.es6 +++ b/src/index.js @@ -17,9 +17,21 @@ * along with Cockpit; If not, see . */ +import "./lib/patternfly-4-cockpit.scss"; + +import "core-js/stable"; + import React from 'react'; import ReactDOM from 'react-dom'; import { Application } from './app.jsx'; +/* + * PF4 overrides need to come after the JSX components imports because + * these are importing CSS stylesheets that we are overriding + * Having the overrides here will ensure that when mini-css-extract-plugin will extract the CSS + * out of the dist/index.js and since it will maintain the order of the imported CSS, + * the overrides will be correctly in the end of our stylesheet. + */ +import "./lib/patternfly-4-overrides.scss"; document.addEventListener("DOMContentLoaded", function () { ReactDOM.render(React.createElement(Application, {}), document.getElementById('app')); diff --git a/src/journal.css b/src/journal.css deleted file mode 100644 index 8d023f9..0000000 --- a/src/journal.css +++ /dev/null @@ -1,134 +0,0 @@ -/* - * This file is part of Cockpit. - * - * Copyright (C) 2015 Red Hat, Inc. - * - * Cockpit is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation; either version 2.1 of the License, or - * (at your option) any later version. - * - * Cockpit is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Cockpit; If not, see . - */ - -.cockpit-log-panel { - border: 0; -} - -.cockpit-log-panel .panel-heading { - background-color: #333; - border-color: #333; - color: #fff; - padding-left: 10px; - padding-top: 5px; - padding-bottom: 5px; - height: auto; -} - -.cockpit-log-panel .panel-body { - padding: 0; - border-bottom: 1px #ddd solid; -} - -.cockpit-log-panel .panel-body .panel-heading { - border-left: 1px #ddd solid; - border-right: 1px #ddd solid; - border-top: 0; - border-bottom: 1px #ddd solid; - background-color: #f5f5f5; - font-weight: bold; - padding-top: 2px; - padding-bottom: 2px; - width: auto; - color: #333; -} - -.cockpit-log-panel > .panel-heading { - margin-top: 15px; -} - -.cockpit-log-panel .cockpit-logline { - border-left: 1px #ddd solid; - border-right: 1px #ddd solid; - background-color: #f5f5f5; - padding-top: 2px; - padding-bottom: 2px; - padding-left: 10px; -} - -.cockpit-logline { - font-family: monospace; - min-width: 310px; - border-bottom: 1px solid #DDD; - border-top: none; -} - -.cockpit-logline > .row > div:first-child { - padding-left: 20px; -} - -.cockpit-log-panel .cockpit-logline:hover { - background-color: #d4edfa; -} - -.cockpit-log-panel > .cockpit-logline:hover { - cursor: pointer; -} - -.cockpit-logmsg-reboot { - font-style: italic; -} - -.cockpit-log-warning { - display: inline-block; - width: 20px; - vertical-align: middle; -} - -.cockpit-log-warning > i { - color: black; -} - -.cockpit-log-time { - display: inline-block; - width: 40px; - vertical-align: middle; -} - -.cockpit-log-service { - width: 200px; - margin-left: 10px; -} - -.cockpit-log-service-container { - display: inline-block; - width: 200px; - margin-left: 10px; -} - -.cockpit-log-service-reduced { - width: -moz-calc(100% - 70px); - width: -webkit-calc(100% - 70px); - width: calc(100% - 70px); -} - -.cockpit-log-message { - width: -moz-calc(100% - 300px); - width: -webkit-calc(100% - 300px); - width: calc(100% - 300px); -} - -.cockpit-log-message, .cockpit-log-service, .cockpit-log-service-reduced { - text-overflow: ellipsis; - overflow: hidden; - display: inline-block; - white-space: nowrap; - vertical-align: middle; -} - diff --git a/src/lib/_fonts.scss b/src/lib/_fonts.scss new file mode 100644 index 0000000..b3cb18a --- /dev/null +++ b/src/lib/_fonts.scss @@ -0,0 +1,36 @@ +/* + * Keep in sync with https://github.com/cockpit-project/cockpit/tree/master/src/base1/_fonts.scss + */ + +@mixin printRedHatFont( +$weightValue: 400, +$weightName: "Regular", +$familyName: "RedHatText", +$style: "normal", +$relative: true +) { + $filePath: "../../static/fonts" + "/" + $familyName + "-" + $weightName; + @font-face { + font-family: $familyName; + src: url('#{$filePath}.woff2') format('woff2'); + font-style: #{$style}; + font-weight: $weightValue; + text-rendering: optimizeLegibility; + } +} + +@include printRedHatFont(700, "Bold", $familyName: "RedHatDisplay"); +@include printRedHatFont(700, "BoldItalic", $style: "italic", $familyName: "RedHatDisplay"); +@include printRedHatFont(300, "Black", $familyName: "RedHatDisplay"); +@include printRedHatFont(300, "BlackItalic", $style: "italic", $familyName: "RedHatDisplay"); +@include printRedHatFont(300, "Italic", $style: "italic", $familyName: "RedHatDisplay"); +@include printRedHatFont(400, "Medium", $familyName: "RedHatDisplay"); +@include printRedHatFont(400, "MediumItalic", $style: "italic", $familyName: "RedHatDisplay"); +@include printRedHatFont(300, "Regular", $familyName: "RedHatDisplay"); + +@include printRedHatFont(300, "Bold"); +@include printRedHatFont(300, "BoldItalic", $style: "italic"); +@include printRedHatFont(300, "Italic"); +@include printRedHatFont(700, "Medium"); +@include printRedHatFont(700, "MediumItalic", $style: "italic"); +@include printRedHatFont(400, "Regular"); diff --git a/src/lib/patternfly-4-cockpit.scss b/src/lib/patternfly-4-cockpit.scss new file mode 100644 index 0000000..40da84e --- /dev/null +++ b/src/lib/patternfly-4-cockpit.scss @@ -0,0 +1,14 @@ +/* + * Keep in sync with https://github.com/cockpit-project/cockpit/tree/master/src/base1/patternfly-4-cockpit.scss + */ + +/* Set fake font and icon path variables - we are going to indentify these through + * string replacement and remove the relevant font-face declarations + */ +$pf-global--font-path: 'patternfly-fonts-fake-path'; +$pf-global--fonticon-path: 'patternfly-icons-fake-path'; +$pf-global--disable-fontawesome: true !default; // Disable Font Awesome 5 Free +@import '@patternfly/patternfly/patternfly-base.scss'; + +/* Import our own fonts since the PF4 font-face rules are filtered out with patternfly.sed */ +@import "./fonts"; diff --git a/src/lib/patternfly-4-overrides.scss b/src/lib/patternfly-4-overrides.scss new file mode 100644 index 0000000..a02cb95 --- /dev/null +++ b/src/lib/patternfly-4-overrides.scss @@ -0,0 +1,43 @@ +/* + * Keep in sync with https://github.com/cockpit-project/cockpit/tree/master/pkg/lib/patternfly-4-overrides.scss + */ + +/*** PF4 overrides ***/ + +/* WORKAROUND: Override word-break bug */ +/* See: https://github.com/patternfly/patternfly-next/issues/2325 */ +.pf-c-table td { + word-break: normal; + overflow-wrap: break-word; +} + +/* WORKAROUND: Dropdown (PF4): Caret is not properly aligned bug */ +/* See: https://github.com/patternfly/patternfly/issues/2715 */ +/* Align the icons inside of all dropdown toggles. */ +/* Part 1 of 2 */ +.pf-c-dropdown__toggle-button { + display: flex; + align-items: center; +} + +/* Make split button dropdowns the same height as their sibling. */ +/* Part 2 of 2 */ +.pf-m-split-button { + align-items: stretch; +} + +/* WORKAROUND: Navigation problems with Tertiary Nav widget on mobile */ +/* See: https://github.com/patternfly/patternfly-design/issues/840 */ +/* Helper mod to wrap pf-c-nav__tertiary */ +.ct-m-nav__tertiary-wrap { + flex-wrap: wrap; + + .pf-c-nav__scroll-button { + display: none; + } +} + +/* Helper mod to center pf-c-nav__tertiary when it wraps */ +.ct-m-nav__tertiary-center { + justify-content: center; +} diff --git a/src/page.css b/src/page.css deleted file mode 100644 index e5690eb..0000000 --- a/src/page.css +++ /dev/null @@ -1,235 +0,0 @@ -a { - cursor: pointer; -} - -.disabled { - pointer-events: auto; -} - -.btn { - min-height: 26px; - min-width: 26px; -} - -.btn.disabled { - pointer-events: auto; -} - -.btn.disabled:hover { - z-index: auto; -} - -a.disabled { - cursor: not-allowed !important; - text-decoration: none; - pointer-events: none; - color: #8b8d8f; -} - -a.disabled:hover { - text-decoration: none; -} - -.dropdown-menu > li > a.disabled, -.dropdown-menu > li > a.disabled:hover, -.dropdown-menu > li > a.disabled:focus { - color: #999999; -} - -.dropdown-menu > li > a.disabled:hover, -.dropdown-menu > li > a.disabled:focus { - text-decoration: none; - background-color: transparent; - background-image: none; - border-color: transparent; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - cursor: default; -} - -/* Limit dropdown menus to 90% of the viewport size */ -.dropdown-menu { - height: auto; - overflow-x: hidden; - max-height: 90vh; -} -/* Align these buttons more nicely */ -.btn.fa-minus, -.btn.fa-plus { - padding-top: 4px; -} - -/* HACK: Workaround for https://github.com/patternfly/patternfly/issues/174*/ - -.page-ct { - margin-top: 20px; -} - -.highlight-ct { - background-color: #d4edfa; -} - -/* Well and Blankslate */ - -.curtains-ct { - top: 0px; - height: 100%; - width: 100%; - position: fixed; -} - -.panel .well { - margin-bottom: 0px; - border: none; - border-radius: 0px; - background-color: #FAFAFA; -} - -.well.blank-slate-pf { - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.05) inset; - padding-top: 40px; -} - -.blank-slate-pf .spinner-lg { - height: 58px; - width: 58px; -} - -/* - * Control buttons such as play and stop - * Used with .btn .btn-default - */ - -.btn-control-ct, -.btn-control-ct:hover { - background-position: center center; - background-size: 16px 16px; - background-repeat: no-repeat; - background-image: none; - -webkit-border-radius: 2; - -moz-border-radius: 2; - border-radius: 2px; - height: 28px; - width: 28px; - box-shadow: none; -} - -.btn-control-ct { - background-color: #eeeeee; -} - -.btn-control-ct:hover { - background-color: #e0e0e0; -} - - -/* On/off switch */ - -.btn-onoff-ct { - margin: 1px 0px; - text-transform: uppercase; -} - -.btn-onoff-ct .btn { - color: transparent; - border-color: #B7B7B7; - padding: 2px 6px 1px 6px; - background-color: white; - background-image: linear-gradient(to bottom, rgb(250, 250, 250) 0px, rgb(237, 237, 237) 100%); - -webkit-box-shadow: none; - box-shadow: none; - width: 37px; -} - -.btn-onoff-ct .btn:first-child { - border-right: #00435F; -} - -.btn-onoff-ct .btn:last-child { - border-left: #00435F; - padding-left: 5px; -} - -.btn-onoff-ct .btn.active { - background-image: none; - width: 36px; -} - -.btn-onoff-ct .btn.active:first-child { - background-color: #0086CF; - color: white; - border-right: 1px solid #0071b0; -} - -.btn-onoff-ct .btn.active:last-child { - color: #000; - border-left: 1px solid #d6d6d6; -} - -.btn-onoff-ct .btn.disabled { - pointer-events: none; - color: transparent !important; -} - -.btn-onoff-ct .btn.active.disabled { - background-color: #888 !important; - color: white !important; -} - -/* Small list inside a dialog */ -/* Alert fixups */ - -/* HACK: word-wrap workaround for long alerts https://github.com/patternfly/patternfly/issues/491 */ - -.modal-content .alert { - text-align: left; - padding-top: 5px; - padding-bottom: 5px; - word-wrap: break-word; -} - -.modal-content .alert .fa { - position: absolute; - left: 10px; - top: 6px; - font-size: 20px; -} - -.modal-content .alert .pficon { - top: 5px; -} - -.alert.alert-danger .fa { - color: #af151a; -} - -/* Dialog patterns */ - -.dialog-wait-ct { - margin-top: 3px; -} - -.dialog-wait-ct .spinner { - display: inline-block; -} - -.dialog-wait-ct span { - vertical-align: 4px; - padding-left: 10px; -} - -.dialog-list-ct { - max-height: 230px; - overflow-x: auto; - border: 1px solid #CCC; - margin-bottom: 0px; -} - -/* HACK: https://github.com/patternfly/patternfly/issues/255 */ -input[type=number] { - padding: 0 0 0 5px; -} - -/* Make a dialog visible */ -.dialog-ct-visible { - display: block; -} diff --git a/src/pkg/lib/cockpit-components-listing.jsx b/src/pkg/lib/cockpit-components-listing.jsx index 15cc683..19eea66 100644 --- a/src/pkg/lib/cockpit-components-listing.jsx +++ b/src/pkg/lib/cockpit-components-listing.jsx @@ -19,7 +19,6 @@ 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 @@ -73,10 +72,10 @@ export class ListingRow extends React.Component { if (!e || e.button !== 0) return; - let willBeExpanded = !this.state.expanded && this.props.tabRenderers.length > 0; + const willBeExpanded = !this.state.expanded && this.props.tabRenderers.length > 0; this.setState({ expanded: willBeExpanded }); - let loadedTabs = {}; + const loadedTabs = {}; // unload all tabs if not expanded if (willBeExpanded) { // see if we should preload some tabs @@ -108,7 +107,7 @@ export class ListingRow extends React.Component { if (!e || e.button !== 0) return; - let selected = !this.state.selected; + const selected = !this.state.selected; this.setState({ selected: selected }); if (this.props.selectChanged) @@ -122,9 +121,9 @@ export class ListingRow extends React.Component { // only consider primary mouse button if (!e || e.button !== 0) return; - let prevTab = this.state.activeTab; + const prevTab = this.state.activeTab; let prevTabPresence = 'default'; - let loadedTabs = this.state.loadedTabs; + const loadedTabs = this.state.loadedTabs; if (prevTab !== tabIdx) { // see if we need to unload the previous tab if ('presence' in this.props.tabRenderers[prevTab]) @@ -142,11 +141,11 @@ export class ListingRow extends React.Component { } render() { - let self = this; + const 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; + const allowNavigate = !!this.props.navigateToItem && !this.state.expanded; - let headerEntries = this.props.columns.map((itm, index) => { + const 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}); else if ('header' in itm && itm.header) @@ -157,23 +156,21 @@ export class ListingRow extends React.Component { return ({itm.name}); }); - let allowExpand = (this.props.tabRenderers.length > 0); + const allowExpand = (this.props.tabRenderers.length > 0); let expandToggle; if (allowExpand) { - expandToggle = - - ; + expandToggle = ; } else { expandToggle = ; } - let listingItemClasses = ["listing-ct-item"]; + const listingItemClasses = ["listing-ct-item"]; if (!allowNavigate) listingItemClasses.push("listing-ct-nonavigate"); if (!allowExpand) listingItemClasses.push("listing-ct-noexpand"); - let allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined); + const allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined); let clickHandler; if (allowSelect) { clickHandler = this.handleSelectClick; @@ -186,24 +183,26 @@ export class ListingRow extends React.Component { clickHandler = this.handleExpandClick; } - let listingItem = ( - + onClick={clickHandler} + > {expandToggle} {headerEntries} ); if (this.state.expanded) { - let links = this.props.tabRenderers.map((itm, idx) => { + const links = this.props.tabRenderers.map((itm, idx) => { return ( -
  • +
  • {itm.name}
  • ); }); - let tabs = []; + const tabs = []; let tabIdx; let Renderer; let rendererData; @@ -289,7 +288,7 @@ ListingRow.propTypes = { * - actions: additional listing-wide actions (displayed next to the list's title) */ export const Listing = (props) => { - let bodyClasses = ["listing", "listing-ct"]; + const bodyClasses = ["listing", "listing-ct"]; if (props.fullWidth) bodyClasses.push("listing-ct-wide"); let headerClasses; diff --git a/src/pkg/lib/journal.js b/src/pkg/lib/journal.js index f92ca93..e809a99 100644 --- a/src/pkg/lib/journal.js +++ b/src/pkg/lib/journal.js @@ -17,64 +17,66 @@ * along with Cockpit; If not, see . */ -(function() { +(function () { "use strict"; var cockpit = require("cockpit"); var Mustache = require("mustache"); - var day_header_template = require('raw-loader!journal_day_header.mustache'); - var line_template = require('raw-loader!journal_line.mustache'); - var reboot_template = require('raw-loader!journal_reboot.mustache'); + var day_header_template = require("raw-loader!journal_day_header.mustache"); + var line_template = require("raw-loader!journal_line.mustache"); + var reboot_template = require("raw-loader!journal_reboot.mustache"); var _ = cockpit.gettext; var C_ = cockpit.gettext; - var journal = { }; + var journal = {}; /** - * journalctl([match, ...], [options]) - * @match: any number of journal match strings - * @options: an object containing further options - * - * Load and (by default) stream journal entries as - * json objects. This function returns a jQuery deferred - * object which delivers the various journal entries. - * - * The various @match strings are journalctl matches. - * Zero, one or more can be specified. They must be in - * string format, or arrays of strings. - * - * The optional @options object can contain the following: - * * "host": the host to load journal from - * * "count": number of entries to load and/or pre-stream. - * Default is 10 - * * "follow": if set to false just load entries and don't - * stream further journal data. Default is true. - * * "directory": optional directory to load journal files - * * "boot": when set only list entries from this specific - * boot id, or if null then the current boot. - * * "since": if specified list entries since the date/time - * * "until": if specified list entries until the date/time - * * "cursor": a cursor to start listing entries from - * * "after": a cursor to start listing entries after - * - * Returns a jQuery deferred promise. You can call these - * functions on the deferred to handle the responses. Note that - * there are additional non-jQuery methods. - * - * .done(function(entries) { }): Called when done, @entries is - * an array of all journal entries loaded. If .stream() - * has been invoked then @entries will be empty. - * .fail(funciton(ex) { }): called if the operation fails - * .stream(function(entries) { }): called when we receive entries - * entries. Called once per batch of journal @entries, - * whether following or not. - * .stop(): stop following or retrieving entries. - */ + * journalctl([match, ...], [options]) + * @match: any number of journal match strings + * @options: an object containing further options + * + * Load and (by default) stream journal entries as + * json objects. This function returns a jQuery deferred + * object which delivers the various journal entries. + * + * The various @match strings are journalctl matches. + * Zero, one or more can be specified. They must be in + * string format, or arrays of strings. + * + * The optional @options object can contain the following: + * * "host": the host to load journal from + * * "count": number of entries to load and/or pre-stream. + * Default is 10 + * * "follow": if set to false just load entries and don't + * stream further journal data. Default is true. + * * "directory": optional directory to load journal files + * * "boot": when set only list entries from this specific + * boot id, or if null then the current boot. + * * "since": if specified list entries since the date/time + * * "until": if specified list entries until the date/time + * * "cursor": a cursor to start listing entries from + * * "after": a cursor to start listing entries after + * + * Returns a jQuery deferred promise. You can call these + * functions on the deferred to handle the responses. Note that + * there are additional non-jQuery methods. + * + * .done(function(entries) { }): Called when done, @entries is + * an array of all journal entries loaded. If .stream() + * has been invoked then @entries will be empty. + * .fail(funciton(ex) { }): called if the operation fails + * .stream(function(entries) { }): called when we receive entries + * entries. Called once per batch of journal @entries, + * whether following or not. + * .stop(): stop following or retrieving entries. + */ journal.journalctl = function journalctl(/* ... */) { var matches = []; - var i, arg, options = { follow: true }; + var i; + var arg; + var options = { follow: true }; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; if (typeof arg == "string") { @@ -92,41 +94,26 @@ } if (options.count === undefined) { - if (options.follow) - options.count = 10; - else - options.count = null; + if (options.follow) options.count = 10; + else options.count = null; } - var cmd = [ "journalctl", "--all", "-q", "--output=json" ]; - if (!options.count) - cmd.push("--no-tail"); - else - cmd.push("--lines=" + options.count); - if (options.directory) - cmd.push("--directory=" + options.directory); - if (options.boot) - cmd.push("--boot=" + options.boot); - else if (options.boot !== undefined) - cmd.push("--boot"); - if (options.since) - cmd.push("--since=" + options.since); - if (options.until) - cmd.push("--until=" + options.until); - if (options.cursor) - cmd.push("--cursor=" + options.cursor); - if (options.after) - cmd.push("--after=" + options.after); - if (options.merge) - cmd.push("-m"); - if (options.grep) - cmd.push("--grep=" + options.grep); + var cmd = ["journalctl", "--all", "-q", "--output=json"]; + if (!options.count) cmd.push("--no-tail"); + else cmd.push("--lines=" + options.count); + if (options.directory) cmd.push("--directory=" + options.directory); + if (options.boot) cmd.push("--boot=" + options.boot); + else if (options.boot !== undefined) cmd.push("--boot"); + if (options.since) cmd.push("--since=" + options.since); + if (options.until) cmd.push("--until=" + options.until); + if (options.cursor) cmd.push("--cursor=" + options.cursor); + if (options.after) cmd.push("--after=" + options.after); + if (options.merge) cmd.push("-m"); + if (options.grep) cmd.push("--grep=" + options.grep); /* journalctl doesn't allow reverse and follow together */ - if (options.reverse) - cmd.push("--reverse"); - else if (options.follow) - cmd.push("--follow"); + if (options.reverse) cmd.push("--reverse"); + else if (options.follow) cmd.push("--follow"); cmd.push("--"); cmd.push.apply(cmd, matches); @@ -151,48 +138,51 @@ } } - var proc = cockpit.spawn(cmd, { host: options.host, batch: 8192, latency: 300, superuser: "try" }). - stream(function(data) { + var proc = cockpit + .spawn(cmd, { + host: options.host, + batch: 8192, + latency: 300, + superuser: "try", + }) + .stream(function (data) { + if (buffer) data = buffer + data; + buffer = ""; - if (buffer) - data = buffer + data; - buffer = ""; - - var lines = data.split("\n"); - var last = lines.length - 1; - lines.forEach(function(line, i) { - if (i == last) { - buffer = line; - } else if (line && line.indexOf("-- ") !== 0) { - try { - entries.push(JSON.parse(line)); - } catch (e) { - console.warn(e, line); + var lines = data.split("\n"); + var last = lines.length - 1; + lines.forEach(function (line, i) { + if (i == last) { + buffer = line; + } else if (line && line.indexOf("-- ") !== 0) { + try { + entries.push(JSON.parse(line)); + } catch (e) { + console.warn(e, line); + } } - } - }); + }); - if (streamers.length && interval === null) - interval = window.setInterval(fire_streamers, 300); - }). - done(function() { - fire_streamers(); - dfd.resolve(entries); - }). - fail(function(ex) { - /* The journalctl command fails when no entries are matched - * so we just ignore this status code */ - if (ex.problem == "cancelled" || - ex.exit_status === 1) { + if (streamers.length && interval === null) + interval = window.setInterval(fire_streamers, 300); + }) + .done(function () { fire_streamers(); dfd.resolve(entries); - } else { - dfd.reject(ex); - } - }). - always(function() { - window.clearInterval(interval); - }); + }) + .fail(function (ex) { + /* The journalctl command fails when no entries are matched + * so we just ignore this status code */ + if (ex.problem == "cancelled" || ex.exit_status === 1) { + fire_streamers(); + dfd.resolve(entries); + } else { + dfd.reject(ex); + } + }) + .always(function () { + window.clearInterval(interval); + }); promise = dfd.promise(); promise.stream = function stream(callback) { @@ -206,20 +196,16 @@ }; journal.printable = function printable(value) { - if (value === undefined || value === null) - return _("[no data]"); - else if (typeof(value) == "string") - return value; + if (value === undefined || value === null) return _("[no data]"); + else if (typeof value == "string") return value; else if (value.length !== undefined) return cockpit.format(_("[$0 bytes of binary data]"), value.length); - else - return _("[binary data]"); + else return _("[binary data]"); }; function output_funcs_for_box(box) { - /* Dereference any jQuery object here */ - if (box.jquery) - box = box[0]; + /* Dereference any jQuery object here */ + if (box.jquery) box = box[0]; Mustache.parse(day_header_template); Mustache.parse(line_template); @@ -227,31 +213,28 @@ function render_line(ident, prio, message, count, time, entry) { var parts = { - 'cursor': entry["__CURSOR"], - 'time': time, - 'message': message, - 'service': ident - }; - if (count > 1) - parts['count'] = count; - if (ident === 'abrt-notification') { - parts['problem'] = true; - parts['service'] = entry['PROBLEM_BINARY']; - } - else if (prio < 4) - parts['warning'] = true; + cursor: entry.__CURSOR, + time: time, + message: message, + service: ident, + }; + if (count > 1) parts.count = count; + if (ident === "abrt-notification") { + parts.problem = true; + parts.service = entry.PROBLEM_BINARY; + } else if (prio < 4) parts.warning = true; return Mustache.render(line_template, parts); } var reboot = _("Reboot"); - var reboot_line = Mustache.render(reboot_template, {'message': reboot} ); + var reboot_line = Mustache.render(reboot_template, { message: reboot }); function render_reboot_separator() { return reboot_line; } function render_day_header(day) { - return Mustache.render(day_header_template, {'day': day} ); + return Mustache.render(day_header_template, { day: day }); } function parse_html(string) { @@ -265,43 +248,37 @@ render_day_header: render_day_header, render_reboot_separator: render_reboot_separator, - append: function(elt) { - if (typeof (elt) == "string") - elt = parse_html(elt); + append: function (elt) { + if (typeof elt == "string") elt = parse_html(elt); box.appendChild(elt); }, - prepend: function(elt) { - if (typeof (elt) == "string") - elt = parse_html(elt); - if (box.firstChild) - box.insertBefore(elt, box.firstChild); - else - box.appendChild(elt); + prepend: function (elt) { + if (typeof elt == "string") elt = parse_html(elt); + if (box.firstChild) box.insertBefore(elt, box.firstChild); + else box.appendChild(elt); }, - remove_last: function() { - if (box.lastChild) - box.removeChild(box.lastChild); + remove_last: function () { + if (box.lastChild) box.removeChild(box.lastChild); }, - remove_first: function() { - if (box.firstChild) - box.removeChild(box.firstChild); + remove_first: function () { + if (box.firstChild) box.removeChild(box.firstChild); }, }; } var month_names = [ - C_("month-name", 'January'), - C_("month-name", 'February'), - C_("month-name", 'March'), - C_("month-name", 'April'), - C_("month-name", 'May'), - C_("month-name", 'June'), - C_("month-name", 'July'), - C_("month-name", 'August'), - C_("month-name", 'September'), - C_("month-name", 'October'), - C_("month-name", 'November'), - C_("month-name", 'December') + C_("month-name", "January"), + C_("month-name", "February"), + C_("month-name", "March"), + C_("month-name", "April"), + C_("month-name", "May"), + C_("month-name", "June"), + C_("month-name", "July"), + C_("month-name", "August"), + C_("month-name", "September"), + C_("month-name", "October"), + C_("month-name", "November"), + C_("month-name", "December"), ]; /* Render the journal entries by passing suitable HTML strings back to @@ -369,13 +346,13 @@ journal.renderer = function renderer(funcs_or_box) { var output_funcs; - if (funcs_or_box.render_line) - output_funcs = funcs_or_box; - else - output_funcs = output_funcs_for_box(funcs_or_box); + if (funcs_or_box.render_line) output_funcs = funcs_or_box; + else output_funcs = output_funcs_for_box(funcs_or_box); function copy_object(o) { - var c = { }; for(var p in o) c[p] = o[p]; return c; + var c = {}; + for (var p in o) c[p] = o[p]; + return c; } // A 'entry' object describes a journal entry in formatted form. @@ -385,31 +362,38 @@ function format_entry(journal_entry) { function pad(n) { var str = n.toFixed(); - if(str.length == 1) - str = '0' + str; + if (str.length == 1) str = "0" + str; return str; } - var d = new Date(journal_entry["__REALTIME_TIMESTAMP"] / 1000); + var d = new Date(journal_entry.__REALTIME_TIMESTAMP / 1000); return { - cursor: journal_entry["__CURSOR"], + cursor: journal_entry.__CURSOR, full: journal_entry, - day: month_names[d.getMonth()] + ' ' + d.getDate().toFixed() + ', ' + d.getFullYear().toFixed(), - time: pad(d.getHours()) + ':' + pad(d.getMinutes()), - bootid: journal_entry["_BOOT_ID"], - ident: journal_entry["SYSLOG_IDENTIFIER"] || journal_entry["_COMM"], - prio: journal_entry["PRIORITY"], - message: journal.printable(journal_entry["MESSAGE"]) + day: + month_names[d.getMonth()] + + " " + + d.getDate().toFixed() + + ", " + + d.getFullYear().toFixed(), + time: pad(d.getHours()) + ":" + pad(d.getMinutes()), + bootid: journal_entry._BOOT_ID, + ident: journal_entry.SYSLOG_IDENTIFIER || journal_entry._COMM, + prio: journal_entry.PRIORITY, + message: journal.printable(journal_entry.MESSAGE), }; } function entry_is_equal(a, b) { - return (a && b && - a.day == b.day && - a.bootid == b.bootid && - a.ident == b.ident && - a.prio == b.prio && - a.message == b.message); + return ( + a && + b && + a.day == b.day && + a.bootid == b.bootid && + a.ident == b.ident && + a.prio == b.prio && + a.message == b.message + ); } // A state object describes a line that should be eventually @@ -428,12 +412,14 @@ // output, followed by the line. function render_state_line(state) { - return output_funcs.render_line(state.entry.ident, - state.entry.prio, - state.entry.message, - state.count, - state.last_time, - state.entry.full); + return output_funcs.render_line( + state.entry.ident, + state.entry.prio, + state.entry.message, + state.count, + state.last_time, + state.entry.full + ); } // We keep the state of the first and last journal lines, @@ -446,7 +432,7 @@ var top_state, bottom_state; - top_state = bottom_state = { }; + top_state = bottom_state = {}; function start_new_line() { // If we now have two lines, split the state @@ -483,7 +469,9 @@ if (entry.bootid != top_state.entry.bootid) output_funcs.prepend(output_funcs.render_reboot_separator()); if (entry.day != top_state.entry.day) - output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day)); + output_funcs.prepend( + output_funcs.render_day_header(top_state.entry.day) + ); } start_new_line(); @@ -497,7 +485,9 @@ function prepend_flush() { top_output(); if (top_state.entry) { - output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day)); + output_funcs.prepend( + output_funcs.render_day_header(top_state.entry.day) + ); top_state.header_present = true; } } @@ -541,48 +531,46 @@ bottom_output(); } - return { prepend: prepend, - prepend_flush: prepend_flush, - append: append, - append_flush: append_flush - }; + return { + prepend: prepend, + prepend_flush: prepend_flush, + append: append, + append_flush: append_flush, + }; }; journal.logbox = function logbox(match, max_entries) { - var entries = [ ]; + var entries = []; var box = document.createElement("div"); function render() { var renderer = journal.renderer(box); - while(box.firstChild) - box.removeChild(box.firstChild); + while (box.firstChild) box.removeChild(box.firstChild); for (var i = 0; i < entries.length; i++) { renderer.prepend(entries[i]); } renderer.prepend_flush(); - if (entries.length > 0) - box.removeAttribute("hidden"); - else - box.setAttribute("hidden", "hidden"); + if (entries.length > 0) box.removeAttribute("hidden"); + else box.setAttribute("hidden", "hidden"); } render(); - var promise = journal.journalctl(match, { count: max_entries }). - stream(function(tail) { - entries = entries.concat(tail); - if (entries.length > max_entries) - entries = entries.slice(-max_entries); - render(); - }). - fail(function(error) { - box.appendChild(document.createTextNode(error.message)); - box.removeAttribute("hidden"); - }); + var promise = journal + .journalctl(match, { count: max_entries }) + .stream(function (tail) { + entries = entries.concat(tail); + if (entries.length > max_entries) entries = entries.slice(-max_entries); + render(); + }) + .fail(function (error) { + box.appendChild(document.createTextNode(error.message)); + box.removeAttribute("hidden"); + }); /* Both a DOM element and a promise */ return promise.promise(box); }; module.exports = journal; -}()); +})(); diff --git a/src/player.css b/src/player.css index 270ab76..54cfe0f 100644 --- a/src/player.css +++ b/src/player.css @@ -37,7 +37,7 @@ #input-textarea { width: 100%; - heigth: 100%; + height: 100%; font-family: monospace; resize: none; } @@ -74,3 +74,15 @@ .session_time { margin-right:5px; } + +.pf-c-progress__indicator:after { + content: ""; + position: relative; + width: 20px; + height: 20px; + border-radius: 10px; + background-color: var(--pf-c-progress__indicator--BackgroundColor); + top: -2px; + left: 10px; + float: right; +} diff --git a/src/player.jsx b/src/player.jsx index b614c05..7022b91 100644 --- a/src/player.jsx +++ b/src/player.jsx @@ -20,16 +20,47 @@ import React from 'react'; import './player.css'; import { Terminal as Term } from 'xterm'; -let cockpit = require("cockpit"); -let _ = cockpit.gettext; -let moment = require("moment"); -let Journal = require("journal"); -let $ = require("jquery"); -require("bootstrap-slider"); +import { + Button, + Chip, + ChipGroup, + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + ExpandableSection, + InputGroup, + Progress, + TextInput, + Toolbar, + ToolbarContent, + ToolbarItem, + ToolbarGroup, +} from '@patternfly/react-core'; +import { + ArrowRightIcon, + ExpandIcon, + PauseIcon, + PlayIcon, + RedoIcon, + SearchMinusIcon, + SearchPlusIcon, + SearchIcon, + MinusIcon, + UndoIcon, + ThumbtackIcon, + MigrationIcon, +} from '@patternfly/react-icons'; +const cockpit = require("cockpit"); +const _ = cockpit.gettext; +const moment = require("moment"); +const Journal = require("journal"); +const $ = require("jquery"); -let padInt = function (n, w) { - let i = Math.floor(n); - let a = Math.abs(i); +const padInt = function (n, w) { + const i = Math.floor(n); + const a = Math.abs(i); let s = a.toString(); for (w -= s.length; w > 0; w--) { s = '0' + s; @@ -37,21 +68,21 @@ let padInt = function (n, w) { return ((i < 0) ? '-' : '') + s; }; -let formatDateTime = function (ms) { +const formatDateTime = function (ms) { return moment(ms).format("YYYY-MM-DD HH:mm:ss"); }; /* * Format a time interval from a number of milliseconds. */ -let formatDuration = function (ms) { +const formatDuration = function (ms) { let v = Math.floor(ms / 1000); - let s = Math.floor(v % 60); + const s = Math.floor(v % 60); v = Math.floor(v / 60); - let m = Math.floor(v % 60); + const m = Math.floor(v % 60); v = Math.floor(v / 60); - let h = Math.floor(v % 24); - let d = Math.floor(v / 24); + const h = Math.floor(v % 24); + const d = Math.floor(v / 24); let str = ''; if (d > 0) { @@ -67,7 +98,7 @@ let formatDuration = function (ms) { return (ms < 0 ? '-' : '') + str; }; -let scrollToBottom = function(id) { +const scrollToBottom = function(id) { const el = document.getElementById(id); if (el) { el.scrollTop = el.scrollHeight; @@ -82,15 +113,15 @@ function ErrorList(props) { } return ( - + <> {list} - + ); } function ErrorItem(props) { return ( -
    +
    @@ -100,7 +131,7 @@ function ErrorItem(props) { ); } -let ErrorService = class { +const ErrorService = class { constructor() { this.addMessage = this.addMessage.bind(this); this.errors = []; @@ -125,7 +156,7 @@ let ErrorService = class { /* * An auto-loading buffer of recording's packets. */ -let PacketBuffer = class { +const PacketBuffer = class { /* * Initialize a buffer. */ @@ -181,7 +212,7 @@ let PacketBuffer = class { /* The journalctl reading the recording */ this.journalctl = Journal.journalctl( this.matchList, - {count: "all", follow: false, merge: true}); + { count: "all", follow: false, merge: true }); this.journalctl.fail(this.handleError); this.journalctl.stream(this.handleStream); this.journalctl.done(this.handleDone); @@ -300,7 +331,7 @@ let PacketBuffer = class { this.pktList.push(pkt); /* Notify any matching listeners */ while (this.idxDfdList.length > 0) { - let idxDfd = this.idxDfdList[0]; + const idxDfd = this.idxDfdList[0]; if (idxDfd[0] < this.pktList.length) { this.idxDfdList.shift(); idxDfd[1].resolve(); @@ -363,10 +394,12 @@ let PacketBuffer = class { break; } if (io.length > 0) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); + this.addPacket({ + pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join() + }); io = []; } this.pos += x; @@ -379,10 +412,12 @@ let PacketBuffer = class { break; } if (io.length > 0 && is_output) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); + this.addPacket({ + pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join() + }); io = []; } is_output = false; @@ -401,10 +436,12 @@ let PacketBuffer = class { break; } if (io.length > 0 && !is_output) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); + this.addPacket({ + pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join() + }); io = []; } is_output = true; @@ -423,16 +460,20 @@ let PacketBuffer = class { break; } if (io.length > 0) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); + this.addPacket({ + pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join() + }); io = []; } - this.addPacket({pos: this.pos, - is_io: false, - width: x, - height: y}); + this.addPacket({ + pos: this.pos, + is_io: false, + width: x, + height: y + }); this.width = x; this.height = y; break; @@ -447,10 +488,12 @@ let PacketBuffer = class { } if (io.length > 0) { - this.addPacket({pos: this.pos, - is_io: true, - is_output: is_output, - io: io.join()}); + this.addPacket({ + pos: this.pos, + is_io: true, + is_output: is_output, + io: io.join() + }); } } @@ -517,7 +560,7 @@ let PacketBuffer = class { if (!('__CURSOR' in e)) { this.handleError("No cursor in a Journal entry"); } - this.cursor = e['__CURSOR']; + this.cursor = e.__CURSOR; } /* TODO Refer to entry number/cursor in errors */ if (!('MESSAGE' in e)) { @@ -525,16 +568,16 @@ let PacketBuffer = class { } /* Parse the entry message */ try { - let utf8decoder = new TextDecoder(); + const utf8decoder = new TextDecoder(); /* Journalctl stores fields with non-printable characters * in an array of raw bytes formatted as unsigned * integers */ - if (Array.isArray(e['MESSAGE'])) { - let u8arr = new Uint8Array(e['MESSAGE']); + if (Array.isArray(e.MESSAGE)) { + const u8arr = new Uint8Array(e.MESSAGE); this.parseMessage(JSON.parse(utf8decoder.decode(u8arr))); } else { - this.parseMessage(JSON.parse(e['MESSAGE'])); + this.parseMessage(JSON.parse(e.MESSAGE)); } } catch (error) { this.handleError(error); @@ -555,8 +598,10 @@ let PacketBuffer = class { /* Continue with the "following" run */ this.journalctl = Journal.journalctl( this.matchList, - {cursor: this.cursor, - follow: true, merge: true, count: "all"}); + { + cursor: this.cursor, + follow: true, merge: true, count: "all" + }); this.journalctl.fail(this.handleError); this.journalctl.stream(this.handleStream); /* NOTE: no "done" handler on purpose */ @@ -582,20 +627,18 @@ class Search extends React.Component { }; } - handleInputChange(event) { + handleInputChange(name, value) { event.preventDefault(); - const name = event.target.name; - const value = event.target.value; - let state = {}; + const state = {}; state[name] = value; this.setState(state); - cockpit.location.go(cockpit.location.path[0], $.extend(cockpit.location.options, {search_rec: value})); + cockpit.location.go(cockpit.location.path[0], $.extend(cockpit.location.options, { search_rec: value })); } handleSearchSubmit() { this.journalctl = Journal.journalctl( this.props.matchList, - {count: "all", follow: false, merge: true, grep: this.state.search}); + { count: "all", follow: false, merge: true, grep: this.state.search }); this.journalctl.fail(this.handleError); this.journalctl.stream(this.handleStream); } @@ -605,9 +648,15 @@ class Search extends React.Component { return JSON.parse(item.MESSAGE); }); items = items.map(item => { - return ; + return ( + + ); }); - this.setState({items: items}); + this.setState({ items: items }); } handleError(data) { @@ -617,7 +666,7 @@ class Search extends React.Component { clearSearchResults() { delete cockpit.location.options.search; cockpit.location.go(cockpit.location.path[0], cockpit.location.options); - this.setState({search: ""}); + this.setState({ search: "" }); this.handleStream([]); } @@ -629,18 +678,28 @@ class Search extends React.Component { render() { return ( -
    -
    - - - - - -
    -
    + + + this.handleInputChange("search", value)} /> + + + + {this.state.items} -
    -
    + + ); } } @@ -655,12 +714,6 @@ class InputPlayer extends React.Component { } } -function Slider(props) { - return ( - - ); -} - export class Player extends React.Component { constructor(props) { super(props); @@ -677,9 +730,6 @@ export class Player extends React.Component { this.speedReset = this.speedReset.bind(this); this.fastForwardToEnd = this.fastForwardToEnd.bind(this); this.skipFrame = this.skipFrame.bind(this); - this.initSlider = this.initSlider.bind(this); - this.slideStart = this.slideStart.bind(this); - this.slideStop = this.slideStop.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.sync = this.sync.bind(this); this.zoomIn = this.zoomIn.bind(this); @@ -692,6 +742,8 @@ export class Player extends React.Component { this.fastForwardToTS = this.fastForwardToTS.bind(this); this.sendInput = this.sendInput.bind(this); this.clearInputPlayer = this.clearInputPlayer.bind(this); + this.handleInfoClick = this.handleInfoClick.bind(this); + this.handleProgressClick = this.handleProgressClick.bind(this); this.state = { cols: 80, @@ -722,6 +774,8 @@ export class Player extends React.Component { scale: 1, input: "", mark: 0, + infoEnabled: false, + curTS: 0, }; this.containerHeight = 400; @@ -731,9 +785,6 @@ export class Player extends React.Component { this.reportError = this.error_service.addMessage; this.buf = new PacketBuffer(this.props.matchList, this.reportError); - /* Slider component */ - this.slider = null; - /* Current recording time, ms */ this.recTS = 0; /* Corresponding local time, ms */ @@ -746,8 +797,6 @@ export class Player extends React.Component { /* Timeout ID of the current packet, null if none */ this.timeout = null; - this.currentTsPost = 0; - /* True if the next packet should be output without delay */ this.skip = false; /* Playback speed */ @@ -757,9 +806,6 @@ export class Player extends React.Component { * Recording time, ms, or null if not fast-forwarding. */ this.fastForwardTo = null; - - /* Track paused state prior to slider movement */ - this.pausedBeforeSlide = true; } reset() { @@ -781,7 +827,6 @@ export class Player extends React.Component { /* Move to beginning of recording */ this.recTS = 0; - this.currentTsPost = parseInt(this.recTS); /* Start the playback time */ this.locTS = performance.now(); @@ -851,7 +896,7 @@ export class Player extends React.Component { sendInput(pkt) { if (pkt) { const current_input = this.state.input; - this.setState({input: current_input + pkt.io}); + this.setState({ input: current_input + pkt.io }); } } @@ -866,7 +911,7 @@ export class Player extends React.Component { for (;;) { /* Get another packet to output, if none */ for (; this.pkt === null; this.pktIdx++) { - let pkt = this.buf.pktList[this.pktIdx]; + const pkt = this.buf.pktList[this.pktIdx]; /* If there are no more packets */ if (pkt === undefined) { /* @@ -886,7 +931,7 @@ export class Player extends React.Component { } /* Get the current local time */ - let nowLocTS = performance.now(); + const nowLocTS = performance.now(); /* Ignore the passed time, if we're paused */ if (this.state.paused) { @@ -898,7 +943,6 @@ export class Player extends React.Component { /* Sync to the local time */ this.locTS = nowLocTS; - this.slider.slider('setAttribute', 'max', this.buf.pos); /* If we are skipping one packet's delay */ if (this.skip) { this.skip = false; @@ -918,10 +962,8 @@ export class Player extends React.Component { return; } else { this.recTS += locDelay * this.speed; - let pktRecDelay = this.pkt.pos - this.recTS; - let pktLocDelay = pktRecDelay / this.speed; - this.currentTsPost = parseInt(this.recTS); - this.slider.slider('setValue', this.currentTsPost); + const pktRecDelay = this.pkt.pos - this.recTS; + const pktLocDelay = pktRecDelay / this.speed; /* If we're more than 5 ms early for this packet */ if (pktLocDelay > 5) { /* Call us again on time, later */ @@ -934,8 +976,7 @@ export class Player extends React.Component { if (this.props.logsEnabled) { this.props.onTsChange(this.pkt.pos); } - this.currentTsPost = parseInt(this.pkt.pos); - this.slider.slider('setValue', this.currentTsPost); + this.setState({ curTS: this.pkt.pos }); /* Output the packet */ if (this.pkt.is_io && !this.pkt.is_output) { @@ -955,37 +996,37 @@ export class Player extends React.Component { } playPauseToggle() { - this.setState({paused: !this.state.paused}); + this.setState({ paused: !this.state.paused }); } play() { - this.setState({paused: false}); + this.setState({ paused: false }); } pause() { - this.setState({paused: true}); + this.setState({ paused: true }); } speedUp() { - let speedExp = this.state.speedExp; + const speedExp = this.state.speedExp; if (speedExp < 4) { - this.setState({speedExp: speedExp + 1}); + this.setState({ speedExp: speedExp + 1 }); } } speedDown() { - let speedExp = this.state.speedExp; + const speedExp = this.state.speedExp; if (speedExp > -4) { - this.setState({speedExp: speedExp - 1}); + this.setState({ speedExp: speedExp - 1 }); } } speedReset() { - this.setState({speedExp: 0}); + this.setState({ speedExp: 0 }); } clearInputPlayer() { - this.setState({input: ""}); + this.setState({ input: "" }); } rewindToStart() { @@ -1015,49 +1056,19 @@ export class Player extends React.Component { this.sync(); } - initSlider() { - this.slider = $("#slider").slider({ - value: 0, - tooltip: "hide", - enabled: false, - }); - this.slider.slider('on', 'slideStart', this.slideStart); - this.slider.slider('on', 'slideStop', this.slideStop); - this.slider.slider('enable'); - } - - slideStart(e) { - /* - * Necessary because moving the slider position updates state.paused, - * which won't represent the actual paused state after this event is - * triggered - */ - this.pausedBeforeSlide = this.state.paused; - this.pause(); - } - - slideStop(e) { - if (this.fastForwardToTS) { - this.fastForwardToTS(e); - if (this.pausedBeforeSlide === false) { - this.play(); - } - } - } - handleKeyDown(event) { - let keyCodesFuncs = { - "P": this.playPauseToggle, + const keyCodesFuncs = { + P: this.playPauseToggle, "}": this.speedUp, "{": this.speedDown, - "Backspace": this.speedReset, + Backspace: this.speedReset, ".": this.skipFrame, - "G": this.fastForwardToEnd, - "R": this.rewindToStart, + G: this.fastForwardToEnd, + R: this.rewindToStart, "+": this.zoomIn, "=": this.zoomIn, "-": this.zoomOut, - "Z": this.fitIn, + Z: this.fitIn, }; if (event.target.nodeName.toLowerCase() !== 'input') { if (keyCodesFuncs[event.key]) { @@ -1088,30 +1099,30 @@ export class Player extends React.Component { } dragPanEnable() { - this.setState({drag_pan: true}); + this.setState({ drag_pan: true }); - let scrollwrap = this.refs.scrollwrap; + const scrollwrap = this.refs.scrollwrap; let clicked = false; let clickX; let clickY; $(this.refs.scrollwrap).on({ - 'mousemove': function(e) { + mousemove: function(e) { clicked && updateScrollPos(e); }, - 'mousedown': function(e) { + mousedown: function(e) { clicked = true; clickY = e.pageY; clickX = e.pageX; }, - 'mouseup': function() { + mouseup: function() { clicked = false; $('html').css('cursor', 'auto'); } }); - let updateScrollPos = function(e) { + const updateScrollPos = function(e) { $('html').css('cursor', 'move'); $(scrollwrap).scrollTop($(scrollwrap).scrollTop() + (clickY - e.pageY)); $(scrollwrap).scrollLeft($(scrollwrap).scrollLeft() + (clickX - e.pageX)); @@ -1119,8 +1130,8 @@ export class Player extends React.Component { } dragPanDisable() { - this.setState({drag_pan: false}); - let scrollwrap = this.refs.scrollwrap; + this.setState({ drag_pan: false }); + const scrollwrap = this.refs.scrollwrap; $(scrollwrap).off("mousemove"); $(scrollwrap).off("mousedown"); $(scrollwrap).off("mouseup"); @@ -1132,7 +1143,7 @@ export class Player extends React.Component { scale = scale + 0.1; this.zoom(scale); } else { - this.setState({term_zoom_max: true}); + this.setState({ term_zoom_max: true }); } } @@ -1142,7 +1153,7 @@ export class Player extends React.Component { scale = scale - 0.1; this.zoom(scale); } else { - this.setState({term_zoom_min: true}); + this.setState({ term_zoom_min: true }); } } @@ -1163,7 +1174,7 @@ export class Player extends React.Component { window.addEventListener("keydown", this.handleKeyDown, false); if (this.refs.wrapper.offsetWidth) { - this.setState({containerWidth: this.refs.wrapper.offsetWidth}); + this.setState({ containerWidth: this.refs.wrapper.offsetWidth }); } /* Open the terminal */ this.state.term.open(this.refs.term); @@ -1171,7 +1182,6 @@ export class Player extends React.Component { /* Reset playback */ this.reset(); this.fastForwardToTS(0); - this.initSlider(); } componentDidUpdate(prevProps, prevState) { @@ -1189,11 +1199,21 @@ export class Player extends React.Component { } } - render() { - let r = this.props.recording; + handleInfoClick() { + this.setState({ infoEnabled: !this.state.infoEnabled }); + } - let speedExp = this.state.speedExp; - let speedFactor = Math.pow(2, Math.abs(speedExp)); + handleProgressClick(e) { + const progress = Math.min(1, Math.max(0, e.clientX / $(".pf-c-progress__bar").width())); + const ts = Math.round(progress * this.buf.pos); + this.fastForwardToTS(ts); + } + + render() { + const r = this.props.recording; + + const speedExp = this.state.speedExp; + const speedFactor = Math.pow(2, Math.abs(speedExp)); let speedStr; if (speedExp > 0) { @@ -1205,156 +1225,246 @@ export class Player extends React.Component { } const style = { - "transform": "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")", - "transformOrigin": "top left", - "display": "inline-block", - "margin": "0 auto", - "position": "absolute", - "top": this.state.term_top_style, - "left": this.state.term_left_style, + transform: "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")", + transformOrigin: "top left", + display: "inline-block", + margin: "0 auto", + position: "absolute", + top: this.state.term_top_style, + left: this.state.term_left_style, }; const scrollwrap = { - "minWidth": "630px", - "height": this.containerHeight + "px", - "backgroundColor": "#f5f5f5", - "overflow": this.state.term_scroll, - "position": "relative", + minWidth: "630px", + height: this.containerHeight + "px", + backgroundColor: "#f5f5f5", + overflow: this.state.term_scroll, + position: "relative", }; - const to_right = { - "float": "right", - }; + const timeStr = formatDuration(this.state.curTS) + + " / " + + formatDuration(this.buf.pos); + + const progress = ( + + ); + + const playbackControls = ( + + + + + + + + + + + + + + + + + + + + {speedStr !== "" && + + + + {speedStr} + + + } + + ); + + const visualControls = ( + + + + + + + + + + + + + + + ); + + const panel = ( + + + {playbackControls} + {visualControls} + + + + + + ); + + const recordingInfo = ( + + { + [ + { name: _("ID"), value: r.id }, + { name: _("Hostname"), value: r.hostname }, + { name: _("Boot ID"), value: r.boot_id }, + { name: _("Session ID"), value: r.session_id }, + { name: _("PID"), value: r.pid }, + { name: _("Start"), value: formatDateTime(r.start) }, + { name: _("End"), value: formatDateTime(r.end) }, + { name: _("Duration"), value: formatDuration(r.end - r.start) }, + { name: _("User"), value: r.user } + ].map((item, index) => + + + {item.name}, + {item.value} + ]} /> + + + ) + } + + ); + + const infoSection = ( + + {recordingInfo} + + ); // ensure react never reuses this div by keying it with the terminal widget return ( - -
    -
    -
    -
    -
    - {this.state.title} -
    -
    -
    -
    -
    -
    -
    - -