Updated UI to use patternfly
* Removed unused css files * Converted all UI elements to patternfly 4 * Implemented config page under same app * Replaced slider with patternfly `Progress` component
This commit is contained in:
parent
46ad9834b3
commit
aa63c3871c
21 changed files with 1612 additions and 2381 deletions
12
src/app.jsx
12
src/app.jsx
|
|
@ -20,28 +20,24 @@
|
||||||
import cockpit from 'cockpit';
|
import cockpit from 'cockpit';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './app.scss';
|
import './app.scss';
|
||||||
|
import View from "./recordings.jsx";
|
||||||
|
|
||||||
const _ = cockpit.gettext;
|
const _ = cockpit.gettext;
|
||||||
|
|
||||||
export class Application extends React.Component {
|
export class Application extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.state = { 'hostname': _("Unknown") };
|
this.state = { hostname: _("Unknown") };
|
||||||
|
|
||||||
cockpit.file('/etc/hostname').read()
|
cockpit.file('/etc/hostname').read()
|
||||||
.done((content) => {
|
.done((content) => {
|
||||||
this.setState({ 'hostname': content.trim() });
|
this.setState({ hostname: content.trim() });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid">
|
<View />
|
||||||
<h2>Starter Kit</h2>
|
|
||||||
<p>
|
|
||||||
{ cockpit.format(_("Running on $0"), this.state.hostname) }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
src/app.scss
Normal file
3
src/app.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
p {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<!--
|
|
||||||
This file is part of Cockpit.
|
|
||||||
|
|
||||||
Copyright (C) 2017 Red Hat, Inc.
|
|
||||||
|
|
||||||
Cockpit is free software; you can redistribute it and/or modify it
|
|
||||||
under the terms of the GNU Lesser General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2.1 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Cockpit is distributed in the hope that it will be useful, but
|
|
||||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
Lesser General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Lesser General Public License
|
|
||||||
along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title translate>Journal</title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<link href="../base1/patternfly.css" rel="stylesheet">
|
|
||||||
<link href="table.css" rel="stylesheet">
|
|
||||||
<link href="recordings.css" rel="stylesheet">
|
|
||||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
|
||||||
<script src="../*/po.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="view"></div>
|
|
||||||
<script type="text/javascript" src="config.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
583
src/config.jsx
583
src/config.jsx
|
|
@ -18,16 +18,38 @@
|
||||||
*/
|
*/
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
let cockpit = require("cockpit");
|
import React from "react";
|
||||||
let React = require("react");
|
import {
|
||||||
let ReactDOM = require("react-dom");
|
Button,
|
||||||
let json = require('comment-json');
|
Form,
|
||||||
let ini = require('ini');
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleInputChange = this.handleInputChange.bind(this);
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.setConfig = this.setConfig.bind(this);
|
this.setConfig = this.setConfig.bind(this);
|
||||||
this.fileReadFailed = this.fileReadFailed.bind(this);
|
this.fileReadFailed = this.fileReadFailed.bind(this);
|
||||||
|
|
@ -37,7 +59,7 @@ class Config extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
config_loaded: false,
|
config_loaded: false,
|
||||||
file_error: false,
|
file_error: false,
|
||||||
submitting: "none",
|
submitting: false,
|
||||||
shell: "",
|
shell: "",
|
||||||
notice: "",
|
notice: "",
|
||||||
latency: "",
|
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) {
|
handleSubmit(event) {
|
||||||
this.setState({submitting:"block"});
|
this.setState({ submitting: true });
|
||||||
let config = {
|
const config = {
|
||||||
shell: this.state.shell,
|
shell: this.state.shell,
|
||||||
notice: this.state.notice,
|
notice: this.state.notice,
|
||||||
latency: parseInt(this.state.latency),
|
latency: parseInt(this.state.latency),
|
||||||
|
|
@ -96,7 +110,7 @@ class Config extends React.Component {
|
||||||
writer: this.state.writer
|
writer: this.state.writer
|
||||||
};
|
};
|
||||||
this.file.replace(config).done(() => {
|
this.file.replace(config).done(() => {
|
||||||
this.setState({submitting:"none"});
|
this.setState({ submitting: false });
|
||||||
})
|
})
|
||||||
.fail((error) => {
|
.fail((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
@ -111,12 +125,12 @@ class Config extends React.Component {
|
||||||
var toReturn = {};
|
var toReturn = {};
|
||||||
|
|
||||||
for (var i in ob) {
|
for (var i in ob) {
|
||||||
if (!ob.hasOwnProperty(i)) continue;
|
if (!Object.prototype.hasOwnProperty.call(ob, i)) continue;
|
||||||
|
|
||||||
if ((typeof ob[i]) == 'object') {
|
if ((typeof ob[i]) == 'object') {
|
||||||
var flatObject = flattenObject(ob[i]);
|
var flatObject = flattenObject(ob[i]);
|
||||||
for (var x in flatObject) {
|
for (var x in flatObject) {
|
||||||
if (!flatObject.hasOwnProperty(x)) continue;
|
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) continue;
|
||||||
|
|
||||||
toReturn[i + '_' + x] = flatObject[x];
|
toReturn[i + '_' + x] = flatObject[x];
|
||||||
}
|
}
|
||||||
|
|
@ -126,13 +140,13 @@ class Config extends React.Component {
|
||||||
}
|
}
|
||||||
return toReturn;
|
return toReturn;
|
||||||
};
|
};
|
||||||
let state = flattenObject(data);
|
const state = flattenObject(data);
|
||||||
state.config_loaded = true;
|
state.config_loaded = true;
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig() {
|
getConfig() {
|
||||||
let proc = cockpit.spawn(["tlog-rec-session", "--configuration"]);
|
const proc = cockpit.spawn(["tlog-rec-session", "--configuration"]);
|
||||||
|
|
||||||
proc.stream((data) => {
|
proc.stream((data) => {
|
||||||
this.setConfig(json.parse(data, null, true));
|
this.setConfig(json.parse(data, null, true));
|
||||||
|
|
@ -146,15 +160,15 @@ class Config extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
readConfig() {
|
readConfig() {
|
||||||
let parseFunc = function(data) {
|
const parseFunc = function(data) {
|
||||||
return json.parse(data, null, true);
|
return json.parse(data, null, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
let stringifyFunc = function(data) {
|
const stringifyFunc = function(data) {
|
||||||
return json.stringify(data, null, true);
|
return json.stringify(data, null, true);
|
||||||
};
|
};
|
||||||
// needed for cockpit.file usage
|
// needed for cockpit.file usage
|
||||||
let syntax_object = {
|
const syntax_object = {
|
||||||
parse: parseFunc,
|
parse: parseFunc,
|
||||||
stringify: stringifyFunc,
|
stringify: stringifyFunc,
|
||||||
};
|
};
|
||||||
|
|
@ -163,17 +177,6 @@ class Config extends React.Component {
|
||||||
syntax: syntax_object,
|
syntax: syntax_object,
|
||||||
superuser: true,
|
superuser: true,
|
||||||
});
|
});
|
||||||
/*
|
|
||||||
let promise = this.file.read();
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
if (data === null) {
|
|
||||||
this.fileReadFailed();
|
|
||||||
}
|
|
||||||
}).fail((data) => {
|
|
||||||
this.fileReadFailed(data);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileReadFailed(reason) {
|
fileReadFailed(reason) {
|
||||||
|
|
@ -187,160 +190,201 @@ class Config extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.config_loaded === false && this.state.file_error === false) {
|
const form =
|
||||||
return (
|
(this.state.config_loaded === false && this.state.file_error === false)
|
||||||
<div>Loading</div>
|
? <Spinner />
|
||||||
|
: (this.state.config_loaded === true && this.state.file_error === false)
|
||||||
|
? (
|
||||||
|
<Form isHorizontal>
|
||||||
|
<FormGroup label={_("Shell")}>
|
||||||
|
<TextInput
|
||||||
|
id="shell"
|
||||||
|
value={this.state.shell}
|
||||||
|
onChange={shell => this.setState({ shell })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Notice")}>
|
||||||
|
<TextInput
|
||||||
|
id="notice"
|
||||||
|
value={this.state.notice}
|
||||||
|
onChange={notice => this.setState({ notice })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Latency")}>
|
||||||
|
<TextInput
|
||||||
|
id="latency"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={this.state.latency}
|
||||||
|
onChange={latency => this.setState({ latency })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Payload Size, bytes")}>
|
||||||
|
<TextInput
|
||||||
|
id="payload"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={this.state.payload}
|
||||||
|
onChange={payload => this.setState({ payload })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Logging")}>
|
||||||
|
<Checkbox
|
||||||
|
id="log_input"
|
||||||
|
isChecked={this.state.log_input}
|
||||||
|
onChange={log_input => this.setState({ log_input })}
|
||||||
|
label={_("User's Input")} />
|
||||||
|
<Checkbox
|
||||||
|
id="log_output"
|
||||||
|
isChecked={this.state.log_output}
|
||||||
|
onChange={log_output => this.setState({ log_output })}
|
||||||
|
label={_("User's Output")} />
|
||||||
|
<Checkbox
|
||||||
|
id="log_window"
|
||||||
|
isChecked={this.state.log_window}
|
||||||
|
onChange={log_window => this.setState({ log_window })}
|
||||||
|
label={_("Window Resize")} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Limit Rate, bytes/sec")}>
|
||||||
|
<TextInput
|
||||||
|
id="limit_rate"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={this.state.limit_rate}
|
||||||
|
onChange={limit_rate => this.setState({ limit_rate })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Burst, bytes")}>
|
||||||
|
<TextInput
|
||||||
|
id="limit_burst"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
value={this.state.limit_burst}
|
||||||
|
onChange={limit_burst => this.setState({ limit_burst })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Logging Limit Action")}>
|
||||||
|
<FormSelect
|
||||||
|
id="limit_action"
|
||||||
|
value={this.state.limit_action}
|
||||||
|
onChange={limit_action => this.setState({ limit_action })}>
|
||||||
|
{[
|
||||||
|
{ value: "", label: "" },
|
||||||
|
{ value: "pass", label: _("Pass") },
|
||||||
|
{ value: "delay", label: _("Delay") },
|
||||||
|
{ value: "drop", label: _("Drop") }
|
||||||
|
].map((option, index) =>
|
||||||
|
<FormSelectOption
|
||||||
|
key={index}
|
||||||
|
value={option.value}
|
||||||
|
label={option.label} />
|
||||||
|
)}
|
||||||
|
</FormSelect>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("File Path")}>
|
||||||
|
<TextInput
|
||||||
|
id="file_path"
|
||||||
|
value={this.state.file_path}
|
||||||
|
onChange={file_path => this.setState({ file_path })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Syslog Facility")}>
|
||||||
|
<TextInput
|
||||||
|
id="syslog_facility"
|
||||||
|
value={this.state.syslog_facility}
|
||||||
|
onChange={syslog_facility =>
|
||||||
|
this.setState({ syslog_facility })} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Syslog Priority")}>
|
||||||
|
<FormSelect
|
||||||
|
id="syslog_priority"
|
||||||
|
value={this.state.syslog_priority}
|
||||||
|
onChange={syslog_priority =>
|
||||||
|
this.setState({ syslog_priority })}>
|
||||||
|
{[
|
||||||
|
{ value: "", label: "" },
|
||||||
|
{ value: "info", label: _("Info") },
|
||||||
|
].map((option, index) =>
|
||||||
|
<FormSelectOption
|
||||||
|
key={index}
|
||||||
|
value={option.value}
|
||||||
|
label={option.label} />
|
||||||
|
)}
|
||||||
|
</FormSelect>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Journal Priority")}>
|
||||||
|
<FormSelect
|
||||||
|
id="journal_priority"
|
||||||
|
value={this.state.journal_priority}
|
||||||
|
onChange={journal_priority =>
|
||||||
|
this.setState({ journal_priority })}>
|
||||||
|
{[
|
||||||
|
{ value: "", label: "" },
|
||||||
|
{ value: "info", label: _("Info") },
|
||||||
|
].map((option, index) =>
|
||||||
|
<FormSelectOption
|
||||||
|
key={index}
|
||||||
|
value={option.value}
|
||||||
|
label={option.label} />
|
||||||
|
)}
|
||||||
|
</FormSelect>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Checkbox
|
||||||
|
id="journal_augment"
|
||||||
|
isChecked={this.state.journal_augment}
|
||||||
|
onChange={journal_augment =>
|
||||||
|
this.setState({ journal_augment })}
|
||||||
|
label={_("Augment")} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={_("Writer")}>
|
||||||
|
<FormSelect
|
||||||
|
id="writer"
|
||||||
|
value={this.state.writer}
|
||||||
|
onChange={writer =>
|
||||||
|
this.setState({ writer })}>
|
||||||
|
{[
|
||||||
|
{ value: "", label: "" },
|
||||||
|
{ value: "journal", label: _("Journal") },
|
||||||
|
{ value: "syslog", label: _("Syslog") },
|
||||||
|
{ value: "file", label: _("File") },
|
||||||
|
].map((option, index) =>
|
||||||
|
<FormSelectOption
|
||||||
|
key={index}
|
||||||
|
value={option.value}
|
||||||
|
label={option.label} />
|
||||||
|
)}
|
||||||
|
</FormSelect>
|
||||||
|
</FormGroup>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button
|
||||||
|
id="btn-save-tlog-conf"
|
||||||
|
variant="primary"
|
||||||
|
onClick={this.handleSubmit}>
|
||||||
|
{_("Save")}
|
||||||
|
</Button>
|
||||||
|
{this.state.submitting === true && <Spinner size="lg" />}
|
||||||
|
</ActionGroup>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Bullseye>
|
||||||
|
<EmptyState variant={EmptyStateVariant.small}>
|
||||||
|
<EmptyStateIcon
|
||||||
|
icon={ExclamationCircleIcon}
|
||||||
|
color={global_danger_color_200.value} />
|
||||||
|
<Title headingLevel="h4" size="lg">
|
||||||
|
{_("There is no configuration file of tlog present in your system.")}
|
||||||
|
</Title>
|
||||||
|
<Title headingLevel="h4" size="lg">
|
||||||
|
{_("Please, check the /etc/tlog/tlog-rec-session.conf or if tlog is installed.")}
|
||||||
|
</Title>
|
||||||
|
<EmptyStateBody>
|
||||||
|
{this.state.file_error}
|
||||||
|
</EmptyStateBody>
|
||||||
|
</EmptyState>
|
||||||
|
</Bullseye>
|
||||||
);
|
);
|
||||||
} else if (this.state.config_loaded === true && this.state.file_error === false) {
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.handleSubmit}>
|
<Card>
|
||||||
<table className="form-table-ct col-sm-3">
|
<CardTitle>General Config</CardTitle>
|
||||||
<tbody>
|
<CardBody style={{ maxWidth: "500px" }}>{form}</CardBody>
|
||||||
<tr>
|
</Card>
|
||||||
<td className="top"><label htmlFor="shell" className="control-label">Shell</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="shell" name="shell" value={this.state.shell}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="notice" className="control-label">Notice</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="notice" name="notice" value={this.state.notice}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="latency" className="control-label">Latency</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="number" step="1" id="latency" name="latency" value={this.state.latency}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="latency" className="control-label">Payload Size, bytes</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="number" step="1" id="payload" name="payload" value={this.state.payload}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="log_input" className="control-label">Log User's Input</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="checkbox" id="log_input" name="log_input" defaultChecked={this.state.log_input} onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="log_output" className="control-label">Log User's Output</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="checkbox" id="log_output" name="log_output" defaultChecked={this.state.log_output} onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="log_window" className="control-label">Log Window Resize</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="checkbox" id="log_window" name="log_window" defaultChecked={this.state.log_window} onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="limit_rate" className="control-label">Limit Rate, bytes/sec</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="number" step="1" id="limit_rate" name="limit_rate" value={this.state.limit_rate}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="limit_burst" className="control-label">Burst, bytes</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="number" step="1" id="limit_burst" name="limit_burst" value={this.state.limit_burst}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="limit_action" className="control-label">Logging Limit Action</label></td>
|
|
||||||
<td>
|
|
||||||
<select name="limit_action" id="limit_action" onChange={this.handleInputChange} value={this.state.limit_action} className="form-control">
|
|
||||||
<option value="" />
|
|
||||||
<option value="pass">Pass</option>
|
|
||||||
<option value="delay">Delay</option>
|
|
||||||
<option value="drop">Drop</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="file_path" className="control-label">File Path</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="file_path" name="file_path" defaultChecked={this.state.file_path}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="syslog_facility" className="control-label">Syslog Facility</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="syslog_facility" name="syslog_facility" value={this.state.syslog_facility}
|
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="syslog_priority" className="control-label">Syslog Priority</label></td>
|
|
||||||
<td>
|
|
||||||
<select name="syslog_priority" id="syslog_priority" onChange={this.handleInputChange} value={this.state.syslog_priority} className="form-control">
|
|
||||||
<option value="" />
|
|
||||||
<option value="info">Info</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="journal_priority" className="control-label">Journal Priority</label></td>
|
|
||||||
<td>
|
|
||||||
<select name="journal_priority" id="journal_priority" onChange={this.handleInputChange} value={this.state.journal_priority} className="form-control">
|
|
||||||
<option value="" />
|
|
||||||
<option value="info">Info</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="journal_augment" className="control-label">Journal Augment</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="checkbox" id="journal_augment" name="journal_augment" defaultChecked={this.state.journal_augment} onChange={this.handleInputChange} />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top"><label htmlFor="writer" className="control-label">Writer</label></td>
|
|
||||||
<td>
|
|
||||||
<select name="writer" id="writer" onChange={this.handleInputChange} value={this.state.writer} className="form-control">
|
|
||||||
<option value="" />
|
|
||||||
<option value="journal">Journal</option>
|
|
||||||
<option value="syslog">Syslog</option>
|
|
||||||
<option value="file">File</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top">
|
|
||||||
<button id="btn-save-tlog-conf" className="btn btn-default" type="submit">Save</button>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span style={{display: this.state.submitting}}>Saving...</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className="alert alert-danger">
|
|
||||||
<span className="pficon pficon-error-circle-o" />
|
|
||||||
<p><strong>There is no configuration file of tlog present in your system.</strong></p>
|
|
||||||
<p>Please, check the /etc/tlog/tlog-rec-session.conf or if tlog is installed.</p>
|
|
||||||
<p><strong>{this.state.file_error}</strong></p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,7 +392,6 @@ class SssdConfig extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.handleInputChange = this.handleInputChange.bind(this);
|
|
||||||
this.setConfig = this.setConfig.bind(this);
|
this.setConfig = this.setConfig.bind(this);
|
||||||
this.confSave = this.confSave.bind(this);
|
this.confSave = this.confSave.bind(this);
|
||||||
this.file = null;
|
this.file = null;
|
||||||
|
|
@ -356,16 +399,20 @@ class SssdConfig extends React.Component {
|
||||||
scope: "",
|
scope: "",
|
||||||
users: "",
|
users: "",
|
||||||
groups: "",
|
groups: "",
|
||||||
submitting: "none",
|
submitting: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
confSave(obj) {
|
confSave(obj) {
|
||||||
this.setState({submitting:"block"});
|
this.setState({ submitting: true });
|
||||||
this.file.replace(obj).done(() => {
|
this.file.replace(obj).done(() => {
|
||||||
cockpit.spawn(["chmod", "600", "/etc/sssd/conf.d/sssd-session-recording.conf"], { "superuser": "require" }).done(() => {
|
cockpit.spawn(
|
||||||
cockpit.spawn(["systemctl", "restart", "sssd"], { "superuser": "require" }).done(() => {
|
["chmod", "600", "/etc/sssd/conf.d/sssd-session-recording.conf"],
|
||||||
this.setState({submitting:"none"});
|
{ superuser: "require" }).done(() => {
|
||||||
|
cockpit.spawn(
|
||||||
|
["systemctl", "restart", "sssd"],
|
||||||
|
{ superuser: "require" }).done(() => {
|
||||||
|
this.setState({ submitting: false });
|
||||||
})
|
})
|
||||||
.fail((data) => console.log(data));
|
.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) {
|
setConfig(data) {
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
const obj = {};
|
const obj = {};
|
||||||
|
|
@ -388,13 +427,13 @@ class SssdConfig extends React.Component {
|
||||||
obj.session_recording.scope = "none";
|
obj.session_recording.scope = "none";
|
||||||
this.confSave(obj);
|
this.confSave(obj);
|
||||||
} else {
|
} else {
|
||||||
const config = {...data['session_recording']};
|
const config = { ...data.session_recording };
|
||||||
this.setState(config);
|
this.setState(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let syntax_object = {
|
const syntax_object = {
|
||||||
parse: ini.parse,
|
parse: ini.parse,
|
||||||
stringify: ini.stringify
|
stringify: ini.stringify
|
||||||
};
|
};
|
||||||
|
|
@ -404,7 +443,7 @@ class SssdConfig extends React.Component {
|
||||||
superuser: true,
|
superuser: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let promise = this.file.read();
|
const promise = this.file.read();
|
||||||
|
|
||||||
promise.done(() => this.file.watch(this.setConfig));
|
promise.done(() => this.file.watch(this.setConfig));
|
||||||
|
|
||||||
|
|
@ -424,95 +463,75 @@ class SssdConfig extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
const form = (
|
||||||
<form onSubmit={this.handleSubmit}>
|
<Form isHorizontal>
|
||||||
<table className="info-table-ct col-md-12">
|
<FormGroup label="Scope">
|
||||||
<tbody>
|
<FormSelect
|
||||||
<tr>
|
id="scope"
|
||||||
<td><label htmlFor="scope">Scope</label></td>
|
|
||||||
<td>
|
|
||||||
<select name="scope" id="scope" className="form-control"
|
|
||||||
value={this.state.scope}
|
value={this.state.scope}
|
||||||
onChange={this.handleInputChange} >
|
onChange={scope => this.setState({ scope })}>
|
||||||
<option value="none">None</option>
|
{[
|
||||||
<option value="some">Some</option>
|
{ value: "none", label: _("None") },
|
||||||
<option value="all">All</option>
|
{ value: "some", label: _("Some") },
|
||||||
</select>
|
{ value: "all", label: _("All") }
|
||||||
</td>
|
].map((option, index) =>
|
||||||
</tr>
|
<FormSelectOption
|
||||||
|
key={index}
|
||||||
|
value={option.value}
|
||||||
|
label={option.label} />
|
||||||
|
)}
|
||||||
|
</FormSelect>
|
||||||
|
</FormGroup>
|
||||||
{this.state.scope === "some" &&
|
{this.state.scope === "some" &&
|
||||||
<tr>
|
<>
|
||||||
<td><label htmlFor="users">Users</label></td>
|
<FormGroup label={_("Users")}>
|
||||||
<td>
|
<TextInput
|
||||||
<input type="text" id="users" name="users"
|
id="users"
|
||||||
value={this.state.users}
|
value={this.state.users}
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
onChange={users => this.setState({ users })}
|
||||||
</td>
|
/>
|
||||||
</tr>
|
</FormGroup>
|
||||||
}
|
<FormGroup label={_("Groups")}>
|
||||||
{this.state.scope === "some" &&
|
<TextInput
|
||||||
<tr>
|
id="groups"
|
||||||
<td><label htmlFor="groups">Groups</label></td>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="groups" name="groups"
|
|
||||||
value={this.state.groups}
|
value={this.state.groups}
|
||||||
className="form-control" onChange={this.handleInputChange} />
|
onChange={groups => this.setState({ groups })}
|
||||||
</td>
|
/>
|
||||||
</tr>
|
</FormGroup>
|
||||||
}
|
</>}
|
||||||
<tr>
|
<ActionGroup>
|
||||||
<td><button id="btn-save-sssd-conf" className="btn btn-default" type="submit">Save</button></td>
|
<Button
|
||||||
<td>
|
id="btn-save-sssd-conf"
|
||||||
<span style={{display: this.state.submitting}}>Saving...</span>
|
variant="primary"
|
||||||
</td>
|
onClick={this.handleSubmit}>
|
||||||
</tr>
|
{_("Save")}
|
||||||
</tbody>
|
</Button>
|
||||||
</table>
|
{this.state.submitting === true && <Spinner size="lg" />}
|
||||||
</form>
|
</ActionGroup>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardTitle>SSSD Config</CardTitle>
|
||||||
|
<CardBody style={{ maxWidth: "500px" }}>{form}</CardBody>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfigView extends React.Component {
|
export function Config () {
|
||||||
render() {
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
cockpit.jump(['session-recording']);
|
cockpit.location.go("/");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid">
|
<>
|
||||||
<div className="row">
|
<Button variant="link" icon={<AngleLeftIcon />} onClick={goBack}>
|
||||||
<div className="col-md-12">
|
{_("Session Recording")}
|
||||||
<ol className="breadcrumb">
|
</Button>
|
||||||
<li><a onClick={goBack}>Session
|
<GeneralConfig />
|
||||||
Recording</a></li>
|
|
||||||
<li className="active">Configuration</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<div className="panel panel-default">
|
|
||||||
<div className="panel-heading"><span>General Configuration</span></div>
|
|
||||||
<div className="panel-body" id="sr_config">
|
|
||||||
<Config />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-4">
|
|
||||||
<div className="panel panel-default">
|
|
||||||
<div className="panel-heading"><span>SSSD Configuration</span></div>
|
|
||||||
<div className="panel-body" id="sssd_config">
|
|
||||||
<SssdConfig />
|
<SssdConfig />
|
||||||
</div>
|
</>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.render(<ConfigView />, document.getElementById('view'));
|
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,20 @@ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title translate>Journal</title>
|
<title translateable="yes">Session Recording</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link href="../base1/patternfly.css" rel="stylesheet">
|
<meta name="description" content="">
|
||||||
<link href="recordings.css" rel="stylesheet">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<script type="text/javascript" src="../base1/jquery.js"></script>
|
|
||||||
|
<link rel="stylesheet" href="index.css">
|
||||||
|
|
||||||
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
<script type="text/javascript" src="../base1/cockpit.js"></script>
|
||||||
<script src="../*/po.js"></script>
|
<script type="text/javascript" src="../*/po.js"></script>
|
||||||
|
<script type="text/javascript" src="index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="pf-m-redhat-form">
|
||||||
<div id="view"></div>
|
<div id="app"></div>
|
||||||
<script type="text/javascript" src="recordings.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,21 @@
|
||||||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "./lib/patternfly-4-cockpit.scss";
|
||||||
|
|
||||||
|
import "core-js/stable";
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Application } from './app.jsx';
|
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 () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
ReactDOM.render(React.createElement(Application, {}), document.getElementById('app'));
|
ReactDOM.render(React.createElement(Application, {}), document.getElementById('app'));
|
||||||
134
src/journal.css
134
src/journal.css
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.cockpit-log-panel {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-panel .panel-heading {
|
|
||||||
background-color: #333;
|
|
||||||
border-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-top: 5px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-panel .panel-body {
|
|
||||||
padding: 0;
|
|
||||||
border-bottom: 1px #ddd solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-panel .panel-body .panel-heading {
|
|
||||||
border-left: 1px #ddd solid;
|
|
||||||
border-right: 1px #ddd solid;
|
|
||||||
border-top: 0;
|
|
||||||
border-bottom: 1px #ddd solid;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
width: auto;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-panel > .panel-heading {
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-panel .cockpit-logline {
|
|
||||||
border-left: 1px #ddd solid;
|
|
||||||
border-right: 1px #ddd solid;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-logline {
|
|
||||||
font-family: monospace;
|
|
||||||
min-width: 310px;
|
|
||||||
border-bottom: 1px solid #DDD;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-logline > .row > div:first-child {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-panel .cockpit-logline:hover {
|
|
||||||
background-color: #d4edfa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-panel > .cockpit-logline:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-logmsg-reboot {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-warning {
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-warning > i {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-time {
|
|
||||||
display: inline-block;
|
|
||||||
width: 40px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-service {
|
|
||||||
width: 200px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-service-container {
|
|
||||||
display: inline-block;
|
|
||||||
width: 200px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-service-reduced {
|
|
||||||
width: -moz-calc(100% - 70px);
|
|
||||||
width: -webkit-calc(100% - 70px);
|
|
||||||
width: calc(100% - 70px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-message {
|
|
||||||
width: -moz-calc(100% - 300px);
|
|
||||||
width: -webkit-calc(100% - 300px);
|
|
||||||
width: calc(100% - 300px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cockpit-log-message, .cockpit-log-service, .cockpit-log-service-reduced {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
36
src/lib/_fonts.scss
Normal file
36
src/lib/_fonts.scss
Normal file
|
|
@ -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");
|
||||||
14
src/lib/patternfly-4-cockpit.scss
Normal file
14
src/lib/patternfly-4-cockpit.scss
Normal file
|
|
@ -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";
|
||||||
43
src/lib/patternfly-4-overrides.scss
Normal file
43
src/lib/patternfly-4-overrides.scss
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
235
src/page.css
235
src/page.css
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './listing.less';
|
|
||||||
|
|
||||||
/* entry for an alert in the listing, can be expanded (with details) or standard
|
/* entry for an alert in the listing, can be expanded (with details) or standard
|
||||||
* rowId optional: an identifier for the row which will be set as "data-row-id" attribute on the <tr>
|
* rowId optional: an identifier for the row which will be set as "data-row-id" attribute on the <tr>
|
||||||
|
|
@ -73,10 +72,10 @@ export class ListingRow extends React.Component {
|
||||||
if (!e || e.button !== 0)
|
if (!e || e.button !== 0)
|
||||||
return;
|
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 });
|
this.setState({ expanded: willBeExpanded });
|
||||||
|
|
||||||
let loadedTabs = {};
|
const loadedTabs = {};
|
||||||
// unload all tabs if not expanded
|
// unload all tabs if not expanded
|
||||||
if (willBeExpanded) {
|
if (willBeExpanded) {
|
||||||
// see if we should preload some tabs
|
// see if we should preload some tabs
|
||||||
|
|
@ -108,7 +107,7 @@ export class ListingRow extends React.Component {
|
||||||
if (!e || e.button !== 0)
|
if (!e || e.button !== 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let selected = !this.state.selected;
|
const selected = !this.state.selected;
|
||||||
this.setState({ selected: selected });
|
this.setState({ selected: selected });
|
||||||
|
|
||||||
if (this.props.selectChanged)
|
if (this.props.selectChanged)
|
||||||
|
|
@ -122,9 +121,9 @@ export class ListingRow extends React.Component {
|
||||||
// only consider primary mouse button
|
// only consider primary mouse button
|
||||||
if (!e || e.button !== 0)
|
if (!e || e.button !== 0)
|
||||||
return;
|
return;
|
||||||
let prevTab = this.state.activeTab;
|
const prevTab = this.state.activeTab;
|
||||||
let prevTabPresence = 'default';
|
let prevTabPresence = 'default';
|
||||||
let loadedTabs = this.state.loadedTabs;
|
const loadedTabs = this.state.loadedTabs;
|
||||||
if (prevTab !== tabIdx) {
|
if (prevTab !== tabIdx) {
|
||||||
// see if we need to unload the previous tab
|
// see if we need to unload the previous tab
|
||||||
if ('presence' in this.props.tabRenderers[prevTab])
|
if ('presence' in this.props.tabRenderers[prevTab])
|
||||||
|
|
@ -142,11 +141,11 @@ export class ListingRow extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
const self = this;
|
||||||
// only enable navigation if a function is provided and the row isn't expanded (prevent accidental navigation)
|
// 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))
|
if (typeof itm === 'string' || typeof itm === 'number' || itm === null || itm === undefined || itm instanceof String || React.isValidElement(itm))
|
||||||
return (<td key={index}>{itm}</td>);
|
return (<td key={index}>{itm}</td>);
|
||||||
else if ('header' in itm && itm.header)
|
else if ('header' in itm && itm.header)
|
||||||
|
|
@ -157,23 +156,21 @@ export class ListingRow extends React.Component {
|
||||||
return (<td key={index}>{itm.name}</td>);
|
return (<td key={index}>{itm.name}</td>);
|
||||||
});
|
});
|
||||||
|
|
||||||
let allowExpand = (this.props.tabRenderers.length > 0);
|
const allowExpand = (this.props.tabRenderers.length > 0);
|
||||||
let expandToggle;
|
let expandToggle;
|
||||||
if (allowExpand) {
|
if (allowExpand) {
|
||||||
expandToggle = <td key="expandToggle" className="listing-ct-toggle" onClick={ allowNavigate ? this.handleExpandClick : undefined }>
|
expandToggle = <td key="expandToggle" className="listing-ct-toggle" onClick={ allowNavigate ? this.handleExpandClick : undefined }><i className="fa fa-fw" /></td>;
|
||||||
<i className="fa fa-fw" />
|
|
||||||
</td>;
|
|
||||||
} else {
|
} else {
|
||||||
expandToggle = <td key="expandToggle-empty" className="listing-ct-toggle" />;
|
expandToggle = <td key="expandToggle-empty" className="listing-ct-toggle" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let listingItemClasses = ["listing-ct-item"];
|
const listingItemClasses = ["listing-ct-item"];
|
||||||
if (!allowNavigate)
|
if (!allowNavigate)
|
||||||
listingItemClasses.push("listing-ct-nonavigate");
|
listingItemClasses.push("listing-ct-nonavigate");
|
||||||
if (!allowExpand)
|
if (!allowExpand)
|
||||||
listingItemClasses.push("listing-ct-noexpand");
|
listingItemClasses.push("listing-ct-noexpand");
|
||||||
|
|
||||||
let allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined);
|
const allowSelect = !(allowNavigate || allowExpand) && (this.state.selected !== undefined);
|
||||||
let clickHandler;
|
let clickHandler;
|
||||||
if (allowSelect) {
|
if (allowSelect) {
|
||||||
clickHandler = this.handleSelectClick;
|
clickHandler = this.handleSelectClick;
|
||||||
|
|
@ -186,24 +183,26 @@ export class ListingRow extends React.Component {
|
||||||
clickHandler = this.handleExpandClick;
|
clickHandler = this.handleExpandClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
let listingItem = (
|
const listingItem = (
|
||||||
<tr data-row-id={ this.props.rowId }
|
<tr
|
||||||
|
data-row-id={ this.props.rowId }
|
||||||
className={ listingItemClasses.join(' ') }
|
className={ listingItemClasses.join(' ') }
|
||||||
onClick={clickHandler}>
|
onClick={clickHandler}
|
||||||
|
>
|
||||||
{expandToggle}
|
{expandToggle}
|
||||||
{headerEntries}
|
{headerEntries}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.expanded) {
|
if (this.state.expanded) {
|
||||||
let links = this.props.tabRenderers.map((itm, idx) => {
|
const links = this.props.tabRenderers.map((itm, idx) => {
|
||||||
return (
|
return (
|
||||||
<li key={idx} className={ (idx === self.state.activeTab) ? "active" : ""}>
|
<li key={idx} className={ (idx === self.state.activeTab) ? "active" : ""}>
|
||||||
<a href="#" tabIndex="0" onClick={ self.handleTabClick.bind(self, idx) }>{itm.name}</a>
|
<a href="#" tabIndex="0" onClick={ self.handleTabClick.bind(self, idx) }>{itm.name}</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
let tabs = [];
|
const tabs = [];
|
||||||
let tabIdx;
|
let tabIdx;
|
||||||
let Renderer;
|
let Renderer;
|
||||||
let rendererData;
|
let rendererData;
|
||||||
|
|
@ -289,7 +288,7 @@ ListingRow.propTypes = {
|
||||||
* - actions: additional listing-wide actions (displayed next to the list's title)
|
* - actions: additional listing-wide actions (displayed next to the list's title)
|
||||||
*/
|
*/
|
||||||
export const Listing = (props) => {
|
export const Listing = (props) => {
|
||||||
let bodyClasses = ["listing", "listing-ct"];
|
const bodyClasses = ["listing", "listing-ct"];
|
||||||
if (props.fullWidth)
|
if (props.fullWidth)
|
||||||
bodyClasses.push("listing-ct-wide");
|
bodyClasses.push("listing-ct-wide");
|
||||||
let headerClasses;
|
let headerClasses;
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,9 @@
|
||||||
|
|
||||||
var cockpit = require("cockpit");
|
var cockpit = require("cockpit");
|
||||||
var Mustache = require("mustache");
|
var Mustache = require("mustache");
|
||||||
var day_header_template = require('raw-loader!journal_day_header.mustache');
|
var day_header_template = require("raw-loader!journal_day_header.mustache");
|
||||||
var line_template = require('raw-loader!journal_line.mustache');
|
var line_template = require("raw-loader!journal_line.mustache");
|
||||||
var reboot_template = require('raw-loader!journal_reboot.mustache');
|
var reboot_template = require("raw-loader!journal_reboot.mustache");
|
||||||
|
|
||||||
var _ = cockpit.gettext;
|
var _ = cockpit.gettext;
|
||||||
var C_ = cockpit.gettext;
|
var C_ = cockpit.gettext;
|
||||||
|
|
@ -74,7 +74,9 @@
|
||||||
|
|
||||||
journal.journalctl = function journalctl(/* ... */) {
|
journal.journalctl = function journalctl(/* ... */) {
|
||||||
var matches = [];
|
var matches = [];
|
||||||
var i, arg, options = { follow: true };
|
var i;
|
||||||
|
var arg;
|
||||||
|
var options = { follow: true };
|
||||||
for (i = 0; i < arguments.length; i++) {
|
for (i = 0; i < arguments.length; i++) {
|
||||||
arg = arguments[i];
|
arg = arguments[i];
|
||||||
if (typeof arg == "string") {
|
if (typeof arg == "string") {
|
||||||
|
|
@ -92,41 +94,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.count === undefined) {
|
if (options.count === undefined) {
|
||||||
if (options.follow)
|
if (options.follow) options.count = 10;
|
||||||
options.count = 10;
|
else options.count = null;
|
||||||
else
|
|
||||||
options.count = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd = ["journalctl", "--all", "-q", "--output=json"];
|
var cmd = ["journalctl", "--all", "-q", "--output=json"];
|
||||||
if (!options.count)
|
if (!options.count) cmd.push("--no-tail");
|
||||||
cmd.push("--no-tail");
|
else cmd.push("--lines=" + options.count);
|
||||||
else
|
if (options.directory) cmd.push("--directory=" + options.directory);
|
||||||
cmd.push("--lines=" + options.count);
|
if (options.boot) cmd.push("--boot=" + options.boot);
|
||||||
if (options.directory)
|
else if (options.boot !== undefined) cmd.push("--boot");
|
||||||
cmd.push("--directory=" + options.directory);
|
if (options.since) cmd.push("--since=" + options.since);
|
||||||
if (options.boot)
|
if (options.until) cmd.push("--until=" + options.until);
|
||||||
cmd.push("--boot=" + options.boot);
|
if (options.cursor) cmd.push("--cursor=" + options.cursor);
|
||||||
else if (options.boot !== undefined)
|
if (options.after) cmd.push("--after=" + options.after);
|
||||||
cmd.push("--boot");
|
if (options.merge) cmd.push("-m");
|
||||||
if (options.since)
|
if (options.grep) cmd.push("--grep=" + options.grep);
|
||||||
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 */
|
/* journalctl doesn't allow reverse and follow together */
|
||||||
if (options.reverse)
|
if (options.reverse) cmd.push("--reverse");
|
||||||
cmd.push("--reverse");
|
else if (options.follow) cmd.push("--follow");
|
||||||
else if (options.follow)
|
|
||||||
cmd.push("--follow");
|
|
||||||
|
|
||||||
cmd.push("--");
|
cmd.push("--");
|
||||||
cmd.push.apply(cmd, matches);
|
cmd.push.apply(cmd, matches);
|
||||||
|
|
@ -151,11 +138,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var proc = cockpit.spawn(cmd, { host: options.host, batch: 8192, latency: 300, superuser: "try" }).
|
var proc = cockpit
|
||||||
stream(function(data) {
|
.spawn(cmd, {
|
||||||
|
host: options.host,
|
||||||
if (buffer)
|
batch: 8192,
|
||||||
data = buffer + data;
|
latency: 300,
|
||||||
|
superuser: "try",
|
||||||
|
})
|
||||||
|
.stream(function (data) {
|
||||||
|
if (buffer) data = buffer + data;
|
||||||
buffer = "";
|
buffer = "";
|
||||||
|
|
||||||
var lines = data.split("\n");
|
var lines = data.split("\n");
|
||||||
|
|
@ -174,23 +165,22 @@
|
||||||
|
|
||||||
if (streamers.length && interval === null)
|
if (streamers.length && interval === null)
|
||||||
interval = window.setInterval(fire_streamers, 300);
|
interval = window.setInterval(fire_streamers, 300);
|
||||||
}).
|
})
|
||||||
done(function() {
|
.done(function () {
|
||||||
fire_streamers();
|
fire_streamers();
|
||||||
dfd.resolve(entries);
|
dfd.resolve(entries);
|
||||||
}).
|
})
|
||||||
fail(function(ex) {
|
.fail(function (ex) {
|
||||||
/* The journalctl command fails when no entries are matched
|
/* The journalctl command fails when no entries are matched
|
||||||
* so we just ignore this status code */
|
* so we just ignore this status code */
|
||||||
if (ex.problem == "cancelled" ||
|
if (ex.problem == "cancelled" || ex.exit_status === 1) {
|
||||||
ex.exit_status === 1) {
|
|
||||||
fire_streamers();
|
fire_streamers();
|
||||||
dfd.resolve(entries);
|
dfd.resolve(entries);
|
||||||
} else {
|
} else {
|
||||||
dfd.reject(ex);
|
dfd.reject(ex);
|
||||||
}
|
}
|
||||||
}).
|
})
|
||||||
always(function() {
|
.always(function () {
|
||||||
window.clearInterval(interval);
|
window.clearInterval(interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -206,20 +196,16 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
journal.printable = function printable(value) {
|
journal.printable = function printable(value) {
|
||||||
if (value === undefined || value === null)
|
if (value === undefined || value === null) return _("[no data]");
|
||||||
return _("[no data]");
|
else if (typeof value == "string") return value;
|
||||||
else if (typeof(value) == "string")
|
|
||||||
return value;
|
|
||||||
else if (value.length !== undefined)
|
else if (value.length !== undefined)
|
||||||
return cockpit.format(_("[$0 bytes of binary data]"), value.length);
|
return cockpit.format(_("[$0 bytes of binary data]"), value.length);
|
||||||
else
|
else return _("[binary data]");
|
||||||
return _("[binary data]");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function output_funcs_for_box(box) {
|
function output_funcs_for_box(box) {
|
||||||
/* Dereference any jQuery object here */
|
/* Dereference any jQuery object here */
|
||||||
if (box.jquery)
|
if (box.jquery) box = box[0];
|
||||||
box = box[0];
|
|
||||||
|
|
||||||
Mustache.parse(day_header_template);
|
Mustache.parse(day_header_template);
|
||||||
Mustache.parse(line_template);
|
Mustache.parse(line_template);
|
||||||
|
|
@ -227,31 +213,28 @@
|
||||||
|
|
||||||
function render_line(ident, prio, message, count, time, entry) {
|
function render_line(ident, prio, message, count, time, entry) {
|
||||||
var parts = {
|
var parts = {
|
||||||
'cursor': entry["__CURSOR"],
|
cursor: entry.__CURSOR,
|
||||||
'time': time,
|
time: time,
|
||||||
'message': message,
|
message: message,
|
||||||
'service': ident
|
service: ident,
|
||||||
};
|
};
|
||||||
if (count > 1)
|
if (count > 1) parts.count = count;
|
||||||
parts['count'] = count;
|
if (ident === "abrt-notification") {
|
||||||
if (ident === 'abrt-notification') {
|
parts.problem = true;
|
||||||
parts['problem'] = true;
|
parts.service = entry.PROBLEM_BINARY;
|
||||||
parts['service'] = entry['PROBLEM_BINARY'];
|
} else if (prio < 4) parts.warning = true;
|
||||||
}
|
|
||||||
else if (prio < 4)
|
|
||||||
parts['warning'] = true;
|
|
||||||
return Mustache.render(line_template, parts);
|
return Mustache.render(line_template, parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
var reboot = _("Reboot");
|
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() {
|
function render_reboot_separator() {
|
||||||
return reboot_line;
|
return reboot_line;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render_day_header(day) {
|
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) {
|
function parse_html(string) {
|
||||||
|
|
@ -266,42 +249,36 @@
|
||||||
render_reboot_separator: render_reboot_separator,
|
render_reboot_separator: render_reboot_separator,
|
||||||
|
|
||||||
append: function (elt) {
|
append: function (elt) {
|
||||||
if (typeof (elt) == "string")
|
if (typeof elt == "string") elt = parse_html(elt);
|
||||||
elt = parse_html(elt);
|
|
||||||
box.appendChild(elt);
|
box.appendChild(elt);
|
||||||
},
|
},
|
||||||
prepend: function (elt) {
|
prepend: function (elt) {
|
||||||
if (typeof (elt) == "string")
|
if (typeof elt == "string") elt = parse_html(elt);
|
||||||
elt = parse_html(elt);
|
if (box.firstChild) box.insertBefore(elt, box.firstChild);
|
||||||
if (box.firstChild)
|
else box.appendChild(elt);
|
||||||
box.insertBefore(elt, box.firstChild);
|
|
||||||
else
|
|
||||||
box.appendChild(elt);
|
|
||||||
},
|
},
|
||||||
remove_last: function () {
|
remove_last: function () {
|
||||||
if (box.lastChild)
|
if (box.lastChild) box.removeChild(box.lastChild);
|
||||||
box.removeChild(box.lastChild);
|
|
||||||
},
|
},
|
||||||
remove_first: function () {
|
remove_first: function () {
|
||||||
if (box.firstChild)
|
if (box.firstChild) box.removeChild(box.firstChild);
|
||||||
box.removeChild(box.firstChild);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var month_names = [
|
var month_names = [
|
||||||
C_("month-name", 'January'),
|
C_("month-name", "January"),
|
||||||
C_("month-name", 'February'),
|
C_("month-name", "February"),
|
||||||
C_("month-name", 'March'),
|
C_("month-name", "March"),
|
||||||
C_("month-name", 'April'),
|
C_("month-name", "April"),
|
||||||
C_("month-name", 'May'),
|
C_("month-name", "May"),
|
||||||
C_("month-name", 'June'),
|
C_("month-name", "June"),
|
||||||
C_("month-name", 'July'),
|
C_("month-name", "July"),
|
||||||
C_("month-name", 'August'),
|
C_("month-name", "August"),
|
||||||
C_("month-name", 'September'),
|
C_("month-name", "September"),
|
||||||
C_("month-name", 'October'),
|
C_("month-name", "October"),
|
||||||
C_("month-name", 'November'),
|
C_("month-name", "November"),
|
||||||
C_("month-name", 'December')
|
C_("month-name", "December"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Render the journal entries by passing suitable HTML strings back to
|
/* Render the journal entries by passing suitable HTML strings back to
|
||||||
|
|
@ -369,13 +346,13 @@
|
||||||
|
|
||||||
journal.renderer = function renderer(funcs_or_box) {
|
journal.renderer = function renderer(funcs_or_box) {
|
||||||
var output_funcs;
|
var output_funcs;
|
||||||
if (funcs_or_box.render_line)
|
if (funcs_or_box.render_line) output_funcs = funcs_or_box;
|
||||||
output_funcs = funcs_or_box;
|
else output_funcs = output_funcs_for_box(funcs_or_box);
|
||||||
else
|
|
||||||
output_funcs = output_funcs_for_box(funcs_or_box);
|
|
||||||
|
|
||||||
function copy_object(o) {
|
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.
|
// A 'entry' object describes a journal entry in formatted form.
|
||||||
|
|
@ -385,31 +362,38 @@
|
||||||
function format_entry(journal_entry) {
|
function format_entry(journal_entry) {
|
||||||
function pad(n) {
|
function pad(n) {
|
||||||
var str = n.toFixed();
|
var str = n.toFixed();
|
||||||
if(str.length == 1)
|
if (str.length == 1) str = "0" + str;
|
||||||
str = '0' + str;
|
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
var d = new Date(journal_entry["__REALTIME_TIMESTAMP"] / 1000);
|
var d = new Date(journal_entry.__REALTIME_TIMESTAMP / 1000);
|
||||||
return {
|
return {
|
||||||
cursor: journal_entry["__CURSOR"],
|
cursor: journal_entry.__CURSOR,
|
||||||
full: journal_entry,
|
full: journal_entry,
|
||||||
day: month_names[d.getMonth()] + ' ' + d.getDate().toFixed() + ', ' + d.getFullYear().toFixed(),
|
day:
|
||||||
time: pad(d.getHours()) + ':' + pad(d.getMinutes()),
|
month_names[d.getMonth()] +
|
||||||
bootid: journal_entry["_BOOT_ID"],
|
" " +
|
||||||
ident: journal_entry["SYSLOG_IDENTIFIER"] || journal_entry["_COMM"],
|
d.getDate().toFixed() +
|
||||||
prio: journal_entry["PRIORITY"],
|
", " +
|
||||||
message: journal.printable(journal_entry["MESSAGE"])
|
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) {
|
function entry_is_equal(a, b) {
|
||||||
return (a && b &&
|
return (
|
||||||
|
a &&
|
||||||
|
b &&
|
||||||
a.day == b.day &&
|
a.day == b.day &&
|
||||||
a.bootid == b.bootid &&
|
a.bootid == b.bootid &&
|
||||||
a.ident == b.ident &&
|
a.ident == b.ident &&
|
||||||
a.prio == b.prio &&
|
a.prio == b.prio &&
|
||||||
a.message == b.message);
|
a.message == b.message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A state object describes a line that should be eventually
|
// A state object describes a line that should be eventually
|
||||||
|
|
@ -428,12 +412,14 @@
|
||||||
// output, followed by the line.
|
// output, followed by the line.
|
||||||
|
|
||||||
function render_state_line(state) {
|
function render_state_line(state) {
|
||||||
return output_funcs.render_line(state.entry.ident,
|
return output_funcs.render_line(
|
||||||
|
state.entry.ident,
|
||||||
state.entry.prio,
|
state.entry.prio,
|
||||||
state.entry.message,
|
state.entry.message,
|
||||||
state.count,
|
state.count,
|
||||||
state.last_time,
|
state.last_time,
|
||||||
state.entry.full);
|
state.entry.full
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We keep the state of the first and last journal lines,
|
// We keep the state of the first and last journal lines,
|
||||||
|
|
@ -483,7 +469,9 @@
|
||||||
if (entry.bootid != top_state.entry.bootid)
|
if (entry.bootid != top_state.entry.bootid)
|
||||||
output_funcs.prepend(output_funcs.render_reboot_separator());
|
output_funcs.prepend(output_funcs.render_reboot_separator());
|
||||||
if (entry.day != top_state.entry.day)
|
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();
|
start_new_line();
|
||||||
|
|
@ -497,7 +485,9 @@
|
||||||
function prepend_flush() {
|
function prepend_flush() {
|
||||||
top_output();
|
top_output();
|
||||||
if (top_state.entry) {
|
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;
|
top_state.header_present = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -541,10 +531,11 @@
|
||||||
bottom_output();
|
bottom_output();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { prepend: prepend,
|
return {
|
||||||
|
prepend: prepend,
|
||||||
prepend_flush: prepend_flush,
|
prepend_flush: prepend_flush,
|
||||||
append: append,
|
append: append,
|
||||||
append_flush: append_flush
|
append_flush: append_flush,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -554,28 +545,25 @@
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
var renderer = journal.renderer(box);
|
var renderer = journal.renderer(box);
|
||||||
while(box.firstChild)
|
while (box.firstChild) box.removeChild(box.firstChild);
|
||||||
box.removeChild(box.firstChild);
|
|
||||||
for (var i = 0; i < entries.length; i++) {
|
for (var i = 0; i < entries.length; i++) {
|
||||||
renderer.prepend(entries[i]);
|
renderer.prepend(entries[i]);
|
||||||
}
|
}
|
||||||
renderer.prepend_flush();
|
renderer.prepend_flush();
|
||||||
if (entries.length > 0)
|
if (entries.length > 0) box.removeAttribute("hidden");
|
||||||
box.removeAttribute("hidden");
|
else box.setAttribute("hidden", "hidden");
|
||||||
else
|
|
||||||
box.setAttribute("hidden", "hidden");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render();
|
render();
|
||||||
|
|
||||||
var promise = journal.journalctl(match, { count: max_entries }).
|
var promise = journal
|
||||||
stream(function(tail) {
|
.journalctl(match, { count: max_entries })
|
||||||
|
.stream(function (tail) {
|
||||||
entries = entries.concat(tail);
|
entries = entries.concat(tail);
|
||||||
if (entries.length > max_entries)
|
if (entries.length > max_entries) entries = entries.slice(-max_entries);
|
||||||
entries = entries.slice(-max_entries);
|
|
||||||
render();
|
render();
|
||||||
}).
|
})
|
||||||
fail(function(error) {
|
.fail(function (error) {
|
||||||
box.appendChild(document.createTextNode(error.message));
|
box.appendChild(document.createTextNode(error.message));
|
||||||
box.removeAttribute("hidden");
|
box.removeAttribute("hidden");
|
||||||
});
|
});
|
||||||
|
|
@ -585,4 +573,4 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = journal;
|
module.exports = journal;
|
||||||
}());
|
})();
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
#input-textarea {
|
#input-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
heigth: 100%;
|
height: 100%;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
@ -74,3 +74,15 @@
|
||||||
.session_time {
|
.session_time {
|
||||||
margin-right:5px;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
642
src/player.jsx
642
src/player.jsx
|
|
@ -20,16 +20,47 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './player.css';
|
import './player.css';
|
||||||
import { Terminal as Term } from 'xterm';
|
import { Terminal as Term } from 'xterm';
|
||||||
let cockpit = require("cockpit");
|
import {
|
||||||
let _ = cockpit.gettext;
|
Button,
|
||||||
let moment = require("moment");
|
Chip,
|
||||||
let Journal = require("journal");
|
ChipGroup,
|
||||||
let $ = require("jquery");
|
DataList,
|
||||||
require("bootstrap-slider");
|
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) {
|
const padInt = function (n, w) {
|
||||||
let i = Math.floor(n);
|
const i = Math.floor(n);
|
||||||
let a = Math.abs(i);
|
const a = Math.abs(i);
|
||||||
let s = a.toString();
|
let s = a.toString();
|
||||||
for (w -= s.length; w > 0; w--) {
|
for (w -= s.length; w > 0; w--) {
|
||||||
s = '0' + s;
|
s = '0' + s;
|
||||||
|
|
@ -37,21 +68,21 @@ let padInt = function (n, w) {
|
||||||
return ((i < 0) ? '-' : '') + s;
|
return ((i < 0) ? '-' : '') + s;
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatDateTime = function (ms) {
|
const formatDateTime = function (ms) {
|
||||||
return moment(ms).format("YYYY-MM-DD HH:mm:ss");
|
return moment(ms).format("YYYY-MM-DD HH:mm:ss");
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Format a time interval from a number of milliseconds.
|
* Format a time interval from a number of milliseconds.
|
||||||
*/
|
*/
|
||||||
let formatDuration = function (ms) {
|
const formatDuration = function (ms) {
|
||||||
let v = Math.floor(ms / 1000);
|
let v = Math.floor(ms / 1000);
|
||||||
let s = Math.floor(v % 60);
|
const s = Math.floor(v % 60);
|
||||||
v = 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);
|
v = Math.floor(v / 60);
|
||||||
let h = Math.floor(v % 24);
|
const h = Math.floor(v % 24);
|
||||||
let d = Math.floor(v / 24);
|
const d = Math.floor(v / 24);
|
||||||
let str = '';
|
let str = '';
|
||||||
|
|
||||||
if (d > 0) {
|
if (d > 0) {
|
||||||
|
|
@ -67,7 +98,7 @@ let formatDuration = function (ms) {
|
||||||
return (ms < 0 ? '-' : '') + str;
|
return (ms < 0 ? '-' : '') + str;
|
||||||
};
|
};
|
||||||
|
|
||||||
let scrollToBottom = function(id) {
|
const scrollToBottom = function(id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.scrollTop = el.scrollHeight;
|
el.scrollTop = el.scrollHeight;
|
||||||
|
|
@ -82,9 +113,9 @@ function ErrorList(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
{list}
|
{list}
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +131,7 @@ function ErrorItem(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ErrorService = class {
|
const ErrorService = class {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.addMessage = this.addMessage.bind(this);
|
this.addMessage = this.addMessage.bind(this);
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
|
|
@ -125,7 +156,7 @@ let ErrorService = class {
|
||||||
/*
|
/*
|
||||||
* An auto-loading buffer of recording's packets.
|
* An auto-loading buffer of recording's packets.
|
||||||
*/
|
*/
|
||||||
let PacketBuffer = class {
|
const PacketBuffer = class {
|
||||||
/*
|
/*
|
||||||
* Initialize a buffer.
|
* Initialize a buffer.
|
||||||
*/
|
*/
|
||||||
|
|
@ -300,7 +331,7 @@ let PacketBuffer = class {
|
||||||
this.pktList.push(pkt);
|
this.pktList.push(pkt);
|
||||||
/* Notify any matching listeners */
|
/* Notify any matching listeners */
|
||||||
while (this.idxDfdList.length > 0) {
|
while (this.idxDfdList.length > 0) {
|
||||||
let idxDfd = this.idxDfdList[0];
|
const idxDfd = this.idxDfdList[0];
|
||||||
if (idxDfd[0] < this.pktList.length) {
|
if (idxDfd[0] < this.pktList.length) {
|
||||||
this.idxDfdList.shift();
|
this.idxDfdList.shift();
|
||||||
idxDfd[1].resolve();
|
idxDfd[1].resolve();
|
||||||
|
|
@ -363,10 +394,12 @@ let PacketBuffer = class {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (io.length > 0) {
|
if (io.length > 0) {
|
||||||
this.addPacket({pos: this.pos,
|
this.addPacket({
|
||||||
|
pos: this.pos,
|
||||||
is_io: true,
|
is_io: true,
|
||||||
is_output: is_output,
|
is_output: is_output,
|
||||||
io: io.join()});
|
io: io.join()
|
||||||
|
});
|
||||||
io = [];
|
io = [];
|
||||||
}
|
}
|
||||||
this.pos += x;
|
this.pos += x;
|
||||||
|
|
@ -379,10 +412,12 @@ let PacketBuffer = class {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (io.length > 0 && is_output) {
|
if (io.length > 0 && is_output) {
|
||||||
this.addPacket({pos: this.pos,
|
this.addPacket({
|
||||||
|
pos: this.pos,
|
||||||
is_io: true,
|
is_io: true,
|
||||||
is_output: is_output,
|
is_output: is_output,
|
||||||
io: io.join()});
|
io: io.join()
|
||||||
|
});
|
||||||
io = [];
|
io = [];
|
||||||
}
|
}
|
||||||
is_output = false;
|
is_output = false;
|
||||||
|
|
@ -401,10 +436,12 @@ let PacketBuffer = class {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (io.length > 0 && !is_output) {
|
if (io.length > 0 && !is_output) {
|
||||||
this.addPacket({pos: this.pos,
|
this.addPacket({
|
||||||
|
pos: this.pos,
|
||||||
is_io: true,
|
is_io: true,
|
||||||
is_output: is_output,
|
is_output: is_output,
|
||||||
io: io.join()});
|
io: io.join()
|
||||||
|
});
|
||||||
io = [];
|
io = [];
|
||||||
}
|
}
|
||||||
is_output = true;
|
is_output = true;
|
||||||
|
|
@ -423,16 +460,20 @@ let PacketBuffer = class {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (io.length > 0) {
|
if (io.length > 0) {
|
||||||
this.addPacket({pos: this.pos,
|
this.addPacket({
|
||||||
|
pos: this.pos,
|
||||||
is_io: true,
|
is_io: true,
|
||||||
is_output: is_output,
|
is_output: is_output,
|
||||||
io: io.join()});
|
io: io.join()
|
||||||
|
});
|
||||||
io = [];
|
io = [];
|
||||||
}
|
}
|
||||||
this.addPacket({pos: this.pos,
|
this.addPacket({
|
||||||
|
pos: this.pos,
|
||||||
is_io: false,
|
is_io: false,
|
||||||
width: x,
|
width: x,
|
||||||
height: y});
|
height: y
|
||||||
|
});
|
||||||
this.width = x;
|
this.width = x;
|
||||||
this.height = y;
|
this.height = y;
|
||||||
break;
|
break;
|
||||||
|
|
@ -447,10 +488,12 @@ let PacketBuffer = class {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (io.length > 0) {
|
if (io.length > 0) {
|
||||||
this.addPacket({pos: this.pos,
|
this.addPacket({
|
||||||
|
pos: this.pos,
|
||||||
is_io: true,
|
is_io: true,
|
||||||
is_output: is_output,
|
is_output: is_output,
|
||||||
io: io.join()});
|
io: io.join()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -517,7 +560,7 @@ let PacketBuffer = class {
|
||||||
if (!('__CURSOR' in e)) {
|
if (!('__CURSOR' in e)) {
|
||||||
this.handleError("No cursor in a Journal entry");
|
this.handleError("No cursor in a Journal entry");
|
||||||
}
|
}
|
||||||
this.cursor = e['__CURSOR'];
|
this.cursor = e.__CURSOR;
|
||||||
}
|
}
|
||||||
/* TODO Refer to entry number/cursor in errors */
|
/* TODO Refer to entry number/cursor in errors */
|
||||||
if (!('MESSAGE' in e)) {
|
if (!('MESSAGE' in e)) {
|
||||||
|
|
@ -525,16 +568,16 @@ let PacketBuffer = class {
|
||||||
}
|
}
|
||||||
/* Parse the entry message */
|
/* Parse the entry message */
|
||||||
try {
|
try {
|
||||||
let utf8decoder = new TextDecoder();
|
const utf8decoder = new TextDecoder();
|
||||||
|
|
||||||
/* Journalctl stores fields with non-printable characters
|
/* Journalctl stores fields with non-printable characters
|
||||||
* in an array of raw bytes formatted as unsigned
|
* in an array of raw bytes formatted as unsigned
|
||||||
* integers */
|
* integers */
|
||||||
if (Array.isArray(e['MESSAGE'])) {
|
if (Array.isArray(e.MESSAGE)) {
|
||||||
let u8arr = new Uint8Array(e['MESSAGE']);
|
const u8arr = new Uint8Array(e.MESSAGE);
|
||||||
this.parseMessage(JSON.parse(utf8decoder.decode(u8arr)));
|
this.parseMessage(JSON.parse(utf8decoder.decode(u8arr)));
|
||||||
} else {
|
} else {
|
||||||
this.parseMessage(JSON.parse(e['MESSAGE']));
|
this.parseMessage(JSON.parse(e.MESSAGE));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
|
|
@ -555,8 +598,10 @@ let PacketBuffer = class {
|
||||||
/* Continue with the "following" run */
|
/* Continue with the "following" run */
|
||||||
this.journalctl = Journal.journalctl(
|
this.journalctl = Journal.journalctl(
|
||||||
this.matchList,
|
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.fail(this.handleError);
|
||||||
this.journalctl.stream(this.handleStream);
|
this.journalctl.stream(this.handleStream);
|
||||||
/* NOTE: no "done" handler on purpose */
|
/* NOTE: no "done" handler on purpose */
|
||||||
|
|
@ -582,11 +627,9 @@ class Search extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange(event) {
|
handleInputChange(name, value) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const name = event.target.name;
|
const state = {};
|
||||||
const value = event.target.value;
|
|
||||||
let state = {};
|
|
||||||
state[name] = value;
|
state[name] = value;
|
||||||
this.setState(state);
|
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 }));
|
||||||
|
|
@ -605,7 +648,13 @@ class Search extends React.Component {
|
||||||
return JSON.parse(item.MESSAGE);
|
return JSON.parse(item.MESSAGE);
|
||||||
});
|
});
|
||||||
items = items.map(item => {
|
items = items.map(item => {
|
||||||
return <SearchEntry key={item.id} fastForwardToTS={this.props.fastForwardToTS} pos={item.pos} />;
|
return (
|
||||||
|
<SearchEntry
|
||||||
|
key={item.id}
|
||||||
|
fastForwardToTS={this.props.fastForwardToTS}
|
||||||
|
pos={item.pos}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
this.setState({ items: items });
|
this.setState({ items: items });
|
||||||
}
|
}
|
||||||
|
|
@ -629,18 +678,28 @@ class Search extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="search-wrap">
|
<ToolbarItem>
|
||||||
<div className="input-group search-component">
|
<InputGroup>
|
||||||
<input type="text" className="form-control" name="search" value={this.state.search} onChange={this.handleInputChange} />
|
<TextInput
|
||||||
<span className="input-group-btn">
|
id="search_rec"
|
||||||
<button className="btn btn-default" onClick={this.handleSearchSubmit}><span className="glyphicon glyphicon-search" /></button>
|
type="search"
|
||||||
<button className="btn btn-default" onClick={this.clearSearchResults}><span className="glyphicon glyphicon-remove" /></button>
|
value={this.state.search}
|
||||||
</span>
|
onChange={value => this.handleInputChange("search", value)} />
|
||||||
</div>
|
<Button
|
||||||
<div className="search-results">
|
variant="control"
|
||||||
|
onClick={this.handleSearchSubmit}>
|
||||||
|
<SearchIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="control"
|
||||||
|
onClick={this.clearSearchResults}>
|
||||||
|
<MinusIcon />
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
|
<ToolbarItem>
|
||||||
{this.state.items}
|
{this.state.items}
|
||||||
</div>
|
</ToolbarItem>
|
||||||
</div>
|
</ToolbarItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -655,12 +714,6 @@ class InputPlayer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Slider(props) {
|
|
||||||
return (
|
|
||||||
<input id="slider" type="text" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Player extends React.Component {
|
export class Player extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
@ -677,9 +730,6 @@ export class Player extends React.Component {
|
||||||
this.speedReset = this.speedReset.bind(this);
|
this.speedReset = this.speedReset.bind(this);
|
||||||
this.fastForwardToEnd = this.fastForwardToEnd.bind(this);
|
this.fastForwardToEnd = this.fastForwardToEnd.bind(this);
|
||||||
this.skipFrame = this.skipFrame.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.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
this.sync = this.sync.bind(this);
|
this.sync = this.sync.bind(this);
|
||||||
this.zoomIn = this.zoomIn.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.fastForwardToTS = this.fastForwardToTS.bind(this);
|
||||||
this.sendInput = this.sendInput.bind(this);
|
this.sendInput = this.sendInput.bind(this);
|
||||||
this.clearInputPlayer = this.clearInputPlayer.bind(this);
|
this.clearInputPlayer = this.clearInputPlayer.bind(this);
|
||||||
|
this.handleInfoClick = this.handleInfoClick.bind(this);
|
||||||
|
this.handleProgressClick = this.handleProgressClick.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
cols: 80,
|
cols: 80,
|
||||||
|
|
@ -722,6 +774,8 @@ export class Player extends React.Component {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
input: "",
|
input: "",
|
||||||
mark: 0,
|
mark: 0,
|
||||||
|
infoEnabled: false,
|
||||||
|
curTS: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.containerHeight = 400;
|
this.containerHeight = 400;
|
||||||
|
|
@ -731,9 +785,6 @@ export class Player extends React.Component {
|
||||||
this.reportError = this.error_service.addMessage;
|
this.reportError = this.error_service.addMessage;
|
||||||
this.buf = new PacketBuffer(this.props.matchList, this.reportError);
|
this.buf = new PacketBuffer(this.props.matchList, this.reportError);
|
||||||
|
|
||||||
/* Slider component */
|
|
||||||
this.slider = null;
|
|
||||||
|
|
||||||
/* Current recording time, ms */
|
/* Current recording time, ms */
|
||||||
this.recTS = 0;
|
this.recTS = 0;
|
||||||
/* Corresponding local time, ms */
|
/* Corresponding local time, ms */
|
||||||
|
|
@ -746,8 +797,6 @@ export class Player extends React.Component {
|
||||||
/* Timeout ID of the current packet, null if none */
|
/* Timeout ID of the current packet, null if none */
|
||||||
this.timeout = null;
|
this.timeout = null;
|
||||||
|
|
||||||
this.currentTsPost = 0;
|
|
||||||
|
|
||||||
/* True if the next packet should be output without delay */
|
/* True if the next packet should be output without delay */
|
||||||
this.skip = false;
|
this.skip = false;
|
||||||
/* Playback speed */
|
/* Playback speed */
|
||||||
|
|
@ -757,9 +806,6 @@ export class Player extends React.Component {
|
||||||
* Recording time, ms, or null if not fast-forwarding.
|
* Recording time, ms, or null if not fast-forwarding.
|
||||||
*/
|
*/
|
||||||
this.fastForwardTo = null;
|
this.fastForwardTo = null;
|
||||||
|
|
||||||
/* Track paused state prior to slider movement */
|
|
||||||
this.pausedBeforeSlide = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
|
@ -781,7 +827,6 @@ export class Player extends React.Component {
|
||||||
|
|
||||||
/* Move to beginning of recording */
|
/* Move to beginning of recording */
|
||||||
this.recTS = 0;
|
this.recTS = 0;
|
||||||
this.currentTsPost = parseInt(this.recTS);
|
|
||||||
/* Start the playback time */
|
/* Start the playback time */
|
||||||
this.locTS = performance.now();
|
this.locTS = performance.now();
|
||||||
|
|
||||||
|
|
@ -866,7 +911,7 @@ export class Player extends React.Component {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
/* Get another packet to output, if none */
|
/* Get another packet to output, if none */
|
||||||
for (; this.pkt === null; this.pktIdx++) {
|
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 there are no more packets */
|
||||||
if (pkt === undefined) {
|
if (pkt === undefined) {
|
||||||
/*
|
/*
|
||||||
|
|
@ -886,7 +931,7 @@ export class Player extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get the current local time */
|
/* Get the current local time */
|
||||||
let nowLocTS = performance.now();
|
const nowLocTS = performance.now();
|
||||||
|
|
||||||
/* Ignore the passed time, if we're paused */
|
/* Ignore the passed time, if we're paused */
|
||||||
if (this.state.paused) {
|
if (this.state.paused) {
|
||||||
|
|
@ -898,7 +943,6 @@ export class Player extends React.Component {
|
||||||
/* Sync to the local time */
|
/* Sync to the local time */
|
||||||
this.locTS = nowLocTS;
|
this.locTS = nowLocTS;
|
||||||
|
|
||||||
this.slider.slider('setAttribute', 'max', this.buf.pos);
|
|
||||||
/* If we are skipping one packet's delay */
|
/* If we are skipping one packet's delay */
|
||||||
if (this.skip) {
|
if (this.skip) {
|
||||||
this.skip = false;
|
this.skip = false;
|
||||||
|
|
@ -918,10 +962,8 @@ export class Player extends React.Component {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.recTS += locDelay * this.speed;
|
this.recTS += locDelay * this.speed;
|
||||||
let pktRecDelay = this.pkt.pos - this.recTS;
|
const pktRecDelay = this.pkt.pos - this.recTS;
|
||||||
let pktLocDelay = pktRecDelay / this.speed;
|
const pktLocDelay = pktRecDelay / this.speed;
|
||||||
this.currentTsPost = parseInt(this.recTS);
|
|
||||||
this.slider.slider('setValue', this.currentTsPost);
|
|
||||||
/* If we're more than 5 ms early for this packet */
|
/* If we're more than 5 ms early for this packet */
|
||||||
if (pktLocDelay > 5) {
|
if (pktLocDelay > 5) {
|
||||||
/* Call us again on time, later */
|
/* Call us again on time, later */
|
||||||
|
|
@ -934,8 +976,7 @@ export class Player extends React.Component {
|
||||||
if (this.props.logsEnabled) {
|
if (this.props.logsEnabled) {
|
||||||
this.props.onTsChange(this.pkt.pos);
|
this.props.onTsChange(this.pkt.pos);
|
||||||
}
|
}
|
||||||
this.currentTsPost = parseInt(this.pkt.pos);
|
this.setState({ curTS: this.pkt.pos });
|
||||||
this.slider.slider('setValue', this.currentTsPost);
|
|
||||||
|
|
||||||
/* Output the packet */
|
/* Output the packet */
|
||||||
if (this.pkt.is_io && !this.pkt.is_output) {
|
if (this.pkt.is_io && !this.pkt.is_output) {
|
||||||
|
|
@ -967,14 +1008,14 @@ export class Player extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
speedUp() {
|
speedUp() {
|
||||||
let speedExp = this.state.speedExp;
|
const speedExp = this.state.speedExp;
|
||||||
if (speedExp < 4) {
|
if (speedExp < 4) {
|
||||||
this.setState({ speedExp: speedExp + 1 });
|
this.setState({ speedExp: speedExp + 1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
speedDown() {
|
speedDown() {
|
||||||
let speedExp = this.state.speedExp;
|
const speedExp = this.state.speedExp;
|
||||||
if (speedExp > -4) {
|
if (speedExp > -4) {
|
||||||
this.setState({ speedExp: speedExp - 1 });
|
this.setState({ speedExp: speedExp - 1 });
|
||||||
}
|
}
|
||||||
|
|
@ -1015,49 +1056,19 @@ export class Player extends React.Component {
|
||||||
this.sync();
|
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) {
|
handleKeyDown(event) {
|
||||||
let keyCodesFuncs = {
|
const keyCodesFuncs = {
|
||||||
"P": this.playPauseToggle,
|
P: this.playPauseToggle,
|
||||||
"}": this.speedUp,
|
"}": this.speedUp,
|
||||||
"{": this.speedDown,
|
"{": this.speedDown,
|
||||||
"Backspace": this.speedReset,
|
Backspace: this.speedReset,
|
||||||
".": this.skipFrame,
|
".": this.skipFrame,
|
||||||
"G": this.fastForwardToEnd,
|
G: this.fastForwardToEnd,
|
||||||
"R": this.rewindToStart,
|
R: this.rewindToStart,
|
||||||
"+": this.zoomIn,
|
"+": this.zoomIn,
|
||||||
"=": this.zoomIn,
|
"=": this.zoomIn,
|
||||||
"-": this.zoomOut,
|
"-": this.zoomOut,
|
||||||
"Z": this.fitIn,
|
Z: this.fitIn,
|
||||||
};
|
};
|
||||||
if (event.target.nodeName.toLowerCase() !== 'input') {
|
if (event.target.nodeName.toLowerCase() !== 'input') {
|
||||||
if (keyCodesFuncs[event.key]) {
|
if (keyCodesFuncs[event.key]) {
|
||||||
|
|
@ -1090,28 +1101,28 @@ export class Player extends React.Component {
|
||||||
dragPanEnable() {
|
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 clicked = false;
|
||||||
let clickX;
|
let clickX;
|
||||||
let clickY;
|
let clickY;
|
||||||
|
|
||||||
$(this.refs.scrollwrap).on({
|
$(this.refs.scrollwrap).on({
|
||||||
'mousemove': function(e) {
|
mousemove: function(e) {
|
||||||
clicked && updateScrollPos(e);
|
clicked && updateScrollPos(e);
|
||||||
},
|
},
|
||||||
'mousedown': function(e) {
|
mousedown: function(e) {
|
||||||
clicked = true;
|
clicked = true;
|
||||||
clickY = e.pageY;
|
clickY = e.pageY;
|
||||||
clickX = e.pageX;
|
clickX = e.pageX;
|
||||||
},
|
},
|
||||||
'mouseup': function() {
|
mouseup: function() {
|
||||||
clicked = false;
|
clicked = false;
|
||||||
$('html').css('cursor', 'auto');
|
$('html').css('cursor', 'auto');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let updateScrollPos = function(e) {
|
const updateScrollPos = function(e) {
|
||||||
$('html').css('cursor', 'move');
|
$('html').css('cursor', 'move');
|
||||||
$(scrollwrap).scrollTop($(scrollwrap).scrollTop() + (clickY - e.pageY));
|
$(scrollwrap).scrollTop($(scrollwrap).scrollTop() + (clickY - e.pageY));
|
||||||
$(scrollwrap).scrollLeft($(scrollwrap).scrollLeft() + (clickX - e.pageX));
|
$(scrollwrap).scrollLeft($(scrollwrap).scrollLeft() + (clickX - e.pageX));
|
||||||
|
|
@ -1120,7 +1131,7 @@ export class Player extends React.Component {
|
||||||
|
|
||||||
dragPanDisable() {
|
dragPanDisable() {
|
||||||
this.setState({ drag_pan: false });
|
this.setState({ drag_pan: false });
|
||||||
let scrollwrap = this.refs.scrollwrap;
|
const scrollwrap = this.refs.scrollwrap;
|
||||||
$(scrollwrap).off("mousemove");
|
$(scrollwrap).off("mousemove");
|
||||||
$(scrollwrap).off("mousedown");
|
$(scrollwrap).off("mousedown");
|
||||||
$(scrollwrap).off("mouseup");
|
$(scrollwrap).off("mouseup");
|
||||||
|
|
@ -1171,7 +1182,6 @@ export class Player extends React.Component {
|
||||||
/* Reset playback */
|
/* Reset playback */
|
||||||
this.reset();
|
this.reset();
|
||||||
this.fastForwardToTS(0);
|
this.fastForwardToTS(0);
|
||||||
this.initSlider();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
|
@ -1189,11 +1199,21 @@ export class Player extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
handleInfoClick() {
|
||||||
let r = this.props.recording;
|
this.setState({ infoEnabled: !this.state.infoEnabled });
|
||||||
|
}
|
||||||
|
|
||||||
let speedExp = this.state.speedExp;
|
handleProgressClick(e) {
|
||||||
let speedFactor = Math.pow(2, Math.abs(speedExp));
|
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;
|
let speedStr;
|
||||||
|
|
||||||
if (speedExp > 0) {
|
if (speedExp > 0) {
|
||||||
|
|
@ -1205,156 +1225,246 @@ export class Player extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
"transform": "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")",
|
transform: "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")",
|
||||||
"transformOrigin": "top left",
|
transformOrigin: "top left",
|
||||||
"display": "inline-block",
|
display: "inline-block",
|
||||||
"margin": "0 auto",
|
margin: "0 auto",
|
||||||
"position": "absolute",
|
position: "absolute",
|
||||||
"top": this.state.term_top_style,
|
top: this.state.term_top_style,
|
||||||
"left": this.state.term_left_style,
|
left: this.state.term_left_style,
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollwrap = {
|
const scrollwrap = {
|
||||||
"minWidth": "630px",
|
minWidth: "630px",
|
||||||
"height": this.containerHeight + "px",
|
height: this.containerHeight + "px",
|
||||||
"backgroundColor": "#f5f5f5",
|
backgroundColor: "#f5f5f5",
|
||||||
"overflow": this.state.term_scroll,
|
overflow: this.state.term_scroll,
|
||||||
"position": "relative",
|
position: "relative",
|
||||||
};
|
};
|
||||||
|
|
||||||
const to_right = {
|
const timeStr = formatDuration(this.state.curTS) +
|
||||||
"float": "right",
|
" / " +
|
||||||
};
|
formatDuration(this.buf.pos);
|
||||||
|
|
||||||
|
const progress = (
|
||||||
|
<Progress
|
||||||
|
min={0}
|
||||||
|
max={this.buf.pos}
|
||||||
|
valueText={timeStr}
|
||||||
|
label={timeStr}
|
||||||
|
value={this.state.curTS}
|
||||||
|
onClick={this.handleProgressClick} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const playbackControls = (
|
||||||
|
<ToolbarGroup variant="icon-button-group">
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-play-pause"
|
||||||
|
title="Play/Pause - Hotkey: p"
|
||||||
|
type="button"
|
||||||
|
onClick={this.playPauseToggle}
|
||||||
|
>
|
||||||
|
{this.state.paused ? <PlayIcon /> : <PauseIcon />}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-skip-frame"
|
||||||
|
title="Skip Frame - Hotkey: ."
|
||||||
|
type="button"
|
||||||
|
onClick={this.skipFrame}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon />
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-restart"
|
||||||
|
title="Restart Playback - Hotkey: Shift-R"
|
||||||
|
type="button"
|
||||||
|
onClick={this.rewindToStart}
|
||||||
|
>
|
||||||
|
<UndoIcon />
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-fast-forward"
|
||||||
|
title="Fast-forward to end - Hotkey: Shift-G"
|
||||||
|
type="button"
|
||||||
|
onClick={this.fastForwardToEnd}
|
||||||
|
>
|
||||||
|
<RedoIcon />
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-speed-down"
|
||||||
|
title="Speed /2 - Hotkey: {"
|
||||||
|
type="button"
|
||||||
|
onClick={this.speedDown}
|
||||||
|
>
|
||||||
|
/2
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-speed-up"
|
||||||
|
title="Speed x2 - Hotkey: }"
|
||||||
|
type="button"
|
||||||
|
onClick={this.speedUp}
|
||||||
|
>
|
||||||
|
x2
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
{speedStr !== "" &&
|
||||||
|
<ToolbarItem>
|
||||||
|
<ChipGroup categoryName="speed">
|
||||||
|
<Chip onClick={this.speedReset}>
|
||||||
|
<span id="player-speed">{speedStr}</span>
|
||||||
|
</Chip>
|
||||||
|
</ChipGroup>
|
||||||
|
</ToolbarItem>}
|
||||||
|
</ToolbarGroup>
|
||||||
|
);
|
||||||
|
|
||||||
|
const visualControls = (
|
||||||
|
<ToolbarGroup variant="icon-button-group" alignment={{ default: 'alignRight' }}>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-drag-pan"
|
||||||
|
title="Drag'n'Pan"
|
||||||
|
onClick={this.dragPan}
|
||||||
|
>
|
||||||
|
{this.state.drag_pan ? <ThumbtackIcon /> : <MigrationIcon />}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-zoom-in"
|
||||||
|
title="Zoom In - Hotkey: ="
|
||||||
|
type="button"
|
||||||
|
onClick={this.zoomIn}
|
||||||
|
disabled={this.state.term_zoom_max}
|
||||||
|
>
|
||||||
|
<SearchPlusIcon />
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-fit-to"
|
||||||
|
title="Fit To - Hotkey: Z"
|
||||||
|
type="button"
|
||||||
|
onClick={this.fitTo}
|
||||||
|
>
|
||||||
|
<ExpandIcon />
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
id="player-zoom-out"
|
||||||
|
title="Zoom Out - Hotkey: -"
|
||||||
|
type="button"
|
||||||
|
onClick={this.zoomOut}
|
||||||
|
disabled={this.state.term_zoom_min}
|
||||||
|
>
|
||||||
|
<SearchMinusIcon />
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>
|
||||||
|
);
|
||||||
|
|
||||||
|
const panel = (
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarContent>
|
||||||
|
{playbackControls}
|
||||||
|
{visualControls}
|
||||||
|
<InputPlayer input={this.state.input} />
|
||||||
|
<Search
|
||||||
|
matchList={this.props.matchList}
|
||||||
|
fastForwardToTS={this.fastForwardToTS}
|
||||||
|
play={this.play}
|
||||||
|
pause={this.pause}
|
||||||
|
paused={this.state.paused}
|
||||||
|
errorService={this.error_service} />
|
||||||
|
<ErrorList list={this.error_service.errors} />
|
||||||
|
</ToolbarContent>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
const recordingInfo = (
|
||||||
|
<DataList isCompact>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{ 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) =>
|
||||||
|
<DataListItem key={index}>
|
||||||
|
<DataListItemRow>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell key="name">{item.name}</DataListCell>,
|
||||||
|
<DataListCell key="value">{item.value}</DataListCell>
|
||||||
|
]} />
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</DataList>
|
||||||
|
);
|
||||||
|
|
||||||
|
const infoSection = (
|
||||||
|
<ExpandableSection
|
||||||
|
id="btn-recording-info"
|
||||||
|
toggleText={_("Recording Info")}
|
||||||
|
onToggle={this.handleInfoClick}
|
||||||
|
isExpanded={this.state.infoEnabled === true}>
|
||||||
|
{recordingInfo}
|
||||||
|
</ExpandableSection>
|
||||||
|
);
|
||||||
|
|
||||||
// ensure react never reuses this div by keying it with the terminal widget
|
// ensure react never reuses this div by keying it with the terminal widget
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<div className="row">
|
|
||||||
<div id="recording-wrap">
|
|
||||||
<div className="col-md-7 player-wrap">
|
|
||||||
<div ref="wrapper" className="panel panel-default">
|
<div ref="wrapper" className="panel panel-default">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<span>{this.state.title}</span>
|
<span>{this.state.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
<div className={(this.state.drag_pan ? "dragnpan" : "")} style={scrollwrap} ref="scrollwrap">
|
<div
|
||||||
<div ref="term" className="console-ct" key={this.state.term} style={style} />
|
className={(this.state.drag_pan ? "dragnpan" : "")}
|
||||||
|
style={scrollwrap}
|
||||||
|
ref="scrollwrap">
|
||||||
|
<div
|
||||||
|
ref="term"
|
||||||
|
className="console-ct"
|
||||||
|
key={this.state.term}
|
||||||
|
style={style} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-footer">
|
{progress}
|
||||||
<Slider />
|
{panel}
|
||||||
<button id="player-play-pause" title="Play/Pause - Hotkey: p" type="button" ref="playbtn"
|
|
||||||
className="btn btn-default btn-lg margin-right-btn play-btn"
|
|
||||||
onClick={this.playPauseToggle}>
|
|
||||||
<i className={"fa fa-" + (this.state.paused ? "play" : "pause")}
|
|
||||||
aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<button id="player-skip-frame" title="Skip Frame - Hotkey: ." type="button"
|
|
||||||
className="btn btn-default btn-lg margin-right-btn"
|
|
||||||
onClick={this.skipFrame}>
|
|
||||||
<i className="fa fa-step-forward" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<button id="player-restart" title="Restart Playback - Hotkey: Shift-R" type="button"
|
|
||||||
className="btn btn-default btn-lg" onClick={this.rewindToStart}>
|
|
||||||
<i className="fa fa-fast-backward" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<button id="player-fast-forward" title="Fast-forward to end - Hotkey: Shift-G" type="button"
|
|
||||||
className="btn btn-default btn-lg margin-right-btn"
|
|
||||||
onClick={this.fastForwardToEnd}>
|
|
||||||
<i className="fa fa-fast-forward" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<button id="player-speed-down" title="Speed /2 - Hotkey: {" type="button"
|
|
||||||
className="btn btn-default btn-lg" onClick={this.speedDown}>
|
|
||||||
/2
|
|
||||||
</button>
|
|
||||||
<button id="player-speed-reset" title="Reset Speed - Hotkey: Backspace" type="button"
|
|
||||||
className="btn btn-default btn-lg" onClick={this.speedReset}>
|
|
||||||
1:1
|
|
||||||
</button>
|
|
||||||
<button id="player-speed-up" title="Speed x2 - Hotkey: }" type="button"
|
|
||||||
className="btn btn-default btn-lg margin-right-btn"
|
|
||||||
onClick={this.speedUp}>
|
|
||||||
x2
|
|
||||||
</button>
|
|
||||||
<span id="player-speed">{speedStr}</span>
|
|
||||||
<span style={to_right}>
|
|
||||||
<span className="session_time">{formatDuration(this.currentTsPost)} / {formatDuration(this.buf.pos)}</span>
|
|
||||||
<button id="player-drag-pan" title="Drag'n'Pan" type="button" className="btn btn-default btn-lg"
|
|
||||||
onClick={this.dragPan}>
|
|
||||||
<i className={"fa fa-" + (this.state.drag_pan ? "hand-rock-o" : "hand-paper-o")}
|
|
||||||
aria-hidden="true" /></button>
|
|
||||||
<button id="player-zoom-in" title="Zoom In - Hotkey: =" type="button" className="btn btn-default btn-lg"
|
|
||||||
onClick={this.zoomIn} disabled={this.state.term_zoom_max}>
|
|
||||||
<i className="fa fa-search-plus" aria-hidden="true" /></button>
|
|
||||||
<button id="player-fit-to" title="Fit To - Hotkey: Z" type="button" className="btn btn-default btn-lg"
|
|
||||||
onClick={this.fitTo}><i className="fa fa-expand" aria-hidden="true" /></button>
|
|
||||||
<button id="player-zoom-out" title="Zoom Out - Hotkey: -" type="button" className="btn btn-default btn-lg"
|
|
||||||
onClick={this.zoomOut} disabled={this.state.term_zoom_min}>
|
|
||||||
<i className="fa fa-search-minus" aria-hidden="true" /></button>
|
|
||||||
</span>
|
|
||||||
<div id="input-player-wrap">
|
|
||||||
<InputPlayer input={this.state.input} />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{infoSection}
|
||||||
<Search matchList={this.props.matchList} fastForwardToTS={this.fastForwardToTS} play={this.play} pause={this.pause} paused={this.state.paused} errorService={this.error_service} />
|
</>
|
||||||
</div>
|
|
||||||
<div className="clearfix" />
|
|
||||||
<ErrorList list={this.error_service.errors} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-5">
|
|
||||||
<div className="panel panel-default">
|
|
||||||
<div className="panel-heading">
|
|
||||||
<span>{_("Recording")}</span>
|
|
||||||
</div>
|
|
||||||
<div className="panel-body">
|
|
||||||
<table className="form-table-ct">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>{_("ID")}</td>
|
|
||||||
<td>{r.id}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("Hostname")}</td>
|
|
||||||
<td>{r.hostname}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("Boot ID")}</td>
|
|
||||||
<td>{r.boot_id}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("Session ID")}</td>
|
|
||||||
<td>{r.session_id}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("PID")}</td>
|
|
||||||
<td>{r.pid}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("Start")}</td>
|
|
||||||
<td>{formatDateTime(r.start)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("End")}</td>
|
|
||||||
<td>{formatDateTime(r.end)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("Duration")}</td>
|
|
||||||
<td>{formatDuration(r.end - r.start)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{_("User")}</td>
|
|
||||||
<td>{r.user}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
40
src/plot.css
40
src/plot.css
|
|
@ -1,40 +0,0 @@
|
||||||
.plot-unit {
|
|
||||||
display: inline-block;
|
|
||||||
width: 28px;
|
|
||||||
font-size: smaller;
|
|
||||||
text-align: right;
|
|
||||||
color: #545454;
|
|
||||||
margin-right: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plot-title {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flot-y-axis .flot-tick-label {
|
|
||||||
width: 28px;
|
|
||||||
margin-right: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flot-x-axis .flot-tick-label {
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-controls {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-zoom-controls .zoom-controls {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-zoom-cursor .zoomable-plot {
|
|
||||||
cursor: ew-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standard-zoom-controls {
|
|
||||||
text-align: right; /* on the right */
|
|
||||||
margin-bottom: -15px; /* overlapping with the title */
|
|
||||||
z-index: 1; /* but on top of it */
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
@ -1,373 +0,0 @@
|
||||||
@import "/page.css";
|
|
||||||
|
|
||||||
@import "./journal.css";
|
|
||||||
@import "./plot.css";
|
|
||||||
@import "./table.css";
|
|
||||||
|
|
||||||
@import "./timer.css";
|
|
||||||
|
|
||||||
a.disabled {
|
|
||||||
text-decoration: none;
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: default;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popover {
|
|
||||||
max-width: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.systime-inline form .pficon-close,
|
|
||||||
.systime-inline form .fa-plus {
|
|
||||||
height: 26px;
|
|
||||||
width: 26px;
|
|
||||||
padding: 4px;
|
|
||||||
float: right;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.systime-inline .form-inline {
|
|
||||||
background: #f4f4f4;
|
|
||||||
border-width: 0 1px 1px 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: #bababa;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.systime-inline .form-inline:first-of-type {
|
|
||||||
border-top: 1px solid #bababa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.systime-inline .form-control {
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.systime-inline .form-group:first-of-type .form-control {
|
|
||||||
margin: 0 4px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.systime-inline .form-group .form-control {
|
|
||||||
width: 214px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make sure error message don't overflow the dialog */
|
|
||||||
|
|
||||||
.realms-op-diagnostics {
|
|
||||||
max-width: 550px;
|
|
||||||
text-align: left;
|
|
||||||
max-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.realms-op-wait-message {
|
|
||||||
margin-left: 10px;
|
|
||||||
float: left;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.realms-op-address-spinner {
|
|
||||||
margin-left: -30px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.realms-op-address-error {
|
|
||||||
margin-left: -30px;
|
|
||||||
margin-top: 2px;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.realms-op-zero-width {
|
|
||||||
width: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.realms-op-error {
|
|
||||||
text-align: left;
|
|
||||||
font-weight: bold;
|
|
||||||
overflow: auto;
|
|
||||||
max-width: 550px;
|
|
||||||
max-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Other styles */
|
|
||||||
|
|
||||||
.fa-red {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-messages {
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
#server-graph-toolbar .dropdown {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#server-graph-toolbar .dropdown-toggle span {
|
|
||||||
width: 6em;
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph {
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 30px 40px;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend i {
|
|
||||||
padding-right: 3px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .cpu-io-wait i {
|
|
||||||
color: #e41a1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .cpu-kernel i {
|
|
||||||
color: #ff7f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .cpu-user i {
|
|
||||||
color: #377eb8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .cpu-nice i {
|
|
||||||
color: #4daf4a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .memory-swap i {
|
|
||||||
color: #e41a1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .memory-cached i {
|
|
||||||
color: #ff7f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .memory-used i {
|
|
||||||
color: #377eb8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-graph-legend .memory-free i {
|
|
||||||
color: #4daf4a;
|
|
||||||
}
|
|
||||||
|
|
||||||
#cpu_status_graph,
|
|
||||||
#memory_status_graph {
|
|
||||||
height: 400px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sich-note-1,
|
|
||||||
#sich-note-2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#shutdown-dialog td {
|
|
||||||
padding-right: 20px;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-dialog .opt {
|
|
||||||
padding: 1px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-dialog .dropdown {
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-group {
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-dialog textarea {
|
|
||||||
resize: none;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-dialog input {
|
|
||||||
display: inline;
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#shutdown-dialog .shutdown-hours,
|
|
||||||
#shutdown-dialog .shutdown-minutes {
|
|
||||||
width: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#system_information_ssh_keys .list-group-item {
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#system_information_hardware_text,
|
|
||||||
#system_information_os_text {
|
|
||||||
overflow: visible;
|
|
||||||
white-space: normal;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
|
||||||
.cockpit-modal-md {
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make sure to not break log message lines in order to preserve information */
|
|
||||||
#journal-entry .info-table-ct td {
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-unit-description {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-unit-data {
|
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-unit-failed {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-action-btn ul {
|
|
||||||
right: 0px;
|
|
||||||
left: auto;
|
|
||||||
min-width: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-panel td:first-child {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-panel span {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-panel td:last-child {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-panel table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#services > .container-fluid {
|
|
||||||
margin-top: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-template input {
|
|
||||||
width: 50em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#journal-current-day-menu dropdown-toggle {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#journal-box {
|
|
||||||
margin-top: 5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#journal-entry-message {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#journal-entry-fields {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra content header */
|
|
||||||
|
|
||||||
.content-header-extra {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
padding: 10px 20px;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 900;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-header-extra .btn-group:not(:first-child) {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override cockpit */
|
|
||||||
.form-table-ct {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct td.top {
|
|
||||||
color: #151515;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Align table caption/title */
|
|
||||||
table.listing.listing-ct > caption {
|
|
||||||
padding: 5px 10px 10px !important;
|
|
||||||
font-weight: 300;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#motd {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listing > tbody > tr {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.margin-right-btn {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-btn {
|
|
||||||
min-width: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort span {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-icon {
|
|
||||||
width:7px;
|
|
||||||
display:inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Align table title */
|
|
||||||
table.listing-ct -> caption {
|
|
||||||
padding-left: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.listing-ct > thead th:last-child, tr.listing-ct-item td:last-child {
|
|
||||||
text-align: left !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
width: 185px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invalid {
|
|
||||||
background-color: #ffdddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.valid {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlighted {
|
|
||||||
background-color: #ededed !important;
|
|
||||||
}
|
|
||||||
|
|
@ -19,26 +19,61 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import {
|
||||||
|
Bullseye,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
DataList,
|
||||||
|
DataListCell,
|
||||||
|
DataListItem,
|
||||||
|
DataListItemCells,
|
||||||
|
DataListItemRow,
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateBody,
|
||||||
|
EmptyStateIcon,
|
||||||
|
EmptyStateVariant,
|
||||||
|
ExpandableSection,
|
||||||
|
Spinner,
|
||||||
|
Title,
|
||||||
|
TextInput,
|
||||||
|
Toolbar,
|
||||||
|
ToolbarContent,
|
||||||
|
ToolbarItem,
|
||||||
|
ToolbarGroup,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import {
|
||||||
|
sortable,
|
||||||
|
SortByDirection,
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
import {
|
||||||
|
AngleLeftIcon,
|
||||||
|
CogIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
PlusIcon,
|
||||||
|
SearchIcon
|
||||||
|
} from "@patternfly/react-icons";
|
||||||
|
import { global_danger_color_200 } from "@patternfly/react-tokens";
|
||||||
|
|
||||||
let $ = require("jquery");
|
const $ = require("jquery");
|
||||||
let cockpit = require("cockpit");
|
const cockpit = require("cockpit");
|
||||||
let _ = cockpit.gettext;
|
const _ = cockpit.gettext;
|
||||||
let moment = require("moment");
|
const moment = require("moment");
|
||||||
let Journal = require("journal");
|
const Journal = require("journal");
|
||||||
let Listing = require("cockpit-components-listing.jsx");
|
const Player = require("./player.jsx");
|
||||||
let Player = require("./player.jsx");
|
const Config = require("./config.jsx");
|
||||||
|
|
||||||
require("bootstrap-datetime-picker/js/bootstrap-datetimepicker.js");
|
|
||||||
require("bootstrap-datetime-picker/css/bootstrap-datetimepicker.css");
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert a number to integer number string and pad with zeroes to
|
* Convert a number to integer number string and pad with zeroes to
|
||||||
* specified width.
|
* specified width.
|
||||||
*/
|
*/
|
||||||
let padInt = function (n, w) {
|
const padInt = function (n, w) {
|
||||||
let i = Math.floor(n);
|
const i = Math.floor(n);
|
||||||
let a = Math.abs(i);
|
const a = Math.abs(i);
|
||||||
let s = a.toString();
|
let s = a.toString();
|
||||||
for (w -= s.length; w > 0; w--) {
|
for (w -= s.length; w > 0; w--) {
|
||||||
s = '0' + s;
|
s = '0' + s;
|
||||||
|
|
@ -49,16 +84,16 @@ let padInt = function (n, w) {
|
||||||
/*
|
/*
|
||||||
* Format date and time for a number of milliseconds since Epoch.
|
* Format date and time for a number of milliseconds since Epoch.
|
||||||
*/
|
*/
|
||||||
let formatDateTime = function (ms) {
|
const formatDateTime = function (ms) {
|
||||||
return moment(ms).format("YYYY-MM-DD HH:mm:ss");
|
return moment(ms).format("YYYY-MM-DD HH:mm:ss");
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatDateTimeOffset = function (ms, offset) {
|
const formatDateTimeOffset = function (ms, offset) {
|
||||||
return moment(ms).utcOffset(offset)
|
return moment(ms).utcOffset(offset)
|
||||||
.format("YYYY-MM-DD HH:mm:ss");
|
.format("YYYY-MM-DD HH:mm:ss");
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatUTC = function(date) {
|
const formatUTC = function(date) {
|
||||||
return moment(date).utc()
|
return moment(date).utc()
|
||||||
.format("YYYY-MM-DD HH:mm:ss") + " UTC";
|
.format("YYYY-MM-DD HH:mm:ss") + " UTC";
|
||||||
};
|
};
|
||||||
|
|
@ -66,14 +101,14 @@ let formatUTC = function(date) {
|
||||||
/*
|
/*
|
||||||
* Format a time interval from a number of milliseconds.
|
* Format a time interval from a number of milliseconds.
|
||||||
*/
|
*/
|
||||||
let formatDuration = function (ms) {
|
const formatDuration = function (ms) {
|
||||||
let v = Math.floor(ms / 1000);
|
let v = Math.floor(ms / 1000);
|
||||||
let s = Math.floor(v % 60);
|
const s = Math.floor(v % 60);
|
||||||
v = 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);
|
v = Math.floor(v / 60);
|
||||||
let h = Math.floor(v % 24);
|
const h = Math.floor(v % 24);
|
||||||
let d = Math.floor(v / 24);
|
const d = Math.floor(v / 24);
|
||||||
let str = '';
|
let str = '';
|
||||||
|
|
||||||
if (d > 0) {
|
if (d > 0) {
|
||||||
|
|
@ -89,100 +124,13 @@ let formatDuration = function (ms) {
|
||||||
return (ms < 0 ? '-' : '') + str;
|
return (ms < 0 ? '-' : '') + str;
|
||||||
};
|
};
|
||||||
|
|
||||||
let parseDate = function(date) {
|
|
||||||
let regex = new RegExp(/^\s*(\d\d\d\d-\d\d-\d\d)(\s+(\d\d:\d\d(:\d\d)?))?\s*$/);
|
|
||||||
|
|
||||||
let captures = regex.exec(date);
|
|
||||||
|
|
||||||
if (captures != null) {
|
|
||||||
let date = captures[1];
|
|
||||||
if (captures[3]) {
|
|
||||||
date = date + " " + captures[3];
|
|
||||||
}
|
|
||||||
if (moment(date, ["YYYY-M-D H:m:s", "YYYY-M-D H:m", "YYYY-M-D"], true).isValid()) {
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (date === "" || date === null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A component representing a date & time picker based on bootstrap-datetime-picker.
|
|
||||||
* Requires jQuery, bootstrap-datetime-picker, moment.js
|
|
||||||
* Properties:
|
|
||||||
* - onChange: function to call on date change event of datepicker.
|
|
||||||
* - value: variable to pass which will be used as initial value.
|
|
||||||
*/
|
|
||||||
class Datetimepicker extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.handleDateChange = this.handleDateChange.bind(this);
|
|
||||||
this.clearField = this.clearField.bind(this);
|
|
||||||
this.state = {
|
|
||||||
invalid: false,
|
|
||||||
date: this.props.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
$(this.refs.datepicker).datetimepicker({
|
|
||||||
format: 'yyyy-mm-dd hh:ii:00',
|
|
||||||
autoclose: true,
|
|
||||||
todayBtn: true,
|
|
||||||
})
|
|
||||||
.on('changeDate', this.handleDateChange);
|
|
||||||
// remove datepicker from input, so it only works by button press
|
|
||||||
$(this.refs.datepicker_input).datetimepicker('remove');
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
$(this.refs.datepicker).datetimepicker('remove');
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDateChange() {
|
|
||||||
const date = $(this.refs.datepicker_input).val();
|
|
||||||
this.setState({invalid: false, date: date});
|
|
||||||
if (!parseDate(date)) {
|
|
||||||
this.setState({invalid: true});
|
|
||||||
} else {
|
|
||||||
this.props.onChange(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearField() {
|
|
||||||
const date = "";
|
|
||||||
this.props.onChange(date);
|
|
||||||
this.setState({date: date, invalid: false});
|
|
||||||
$(this.refs.datepicker_input).val("");
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div ref="datepicker" className="input-group date input-append date form_datetime">
|
|
||||||
<input ref="datepicker_input" type="text" size="16"
|
|
||||||
className={"form-control bootstrap-datepicker " + (this.state.invalid ? "invalid" : "valid")}
|
|
||||||
value={this.state.date} onChange={this.handleDateChange} />
|
|
||||||
<span className="input-group-addon add-on"><i className="fa fa-calendar" /></span>
|
|
||||||
<span className="input-group-addon add-on" onClick={this.clearField}>
|
|
||||||
<i className="fa fa-remove" /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogElement(props) {
|
function LogElement(props) {
|
||||||
const entry = props.entry;
|
const entry = props.entry;
|
||||||
const start = props.start;
|
const start = props.start;
|
||||||
const end = props.end;
|
|
||||||
const cursor = entry.__CURSOR;
|
const cursor = entry.__CURSOR;
|
||||||
const entry_timestamp = parseInt(entry.__REALTIME_TIMESTAMP / 1000);
|
const entry_timestamp = parseInt(entry.__REALTIME_TIMESTAMP / 1000);
|
||||||
|
|
||||||
const timeClick = function(e) {
|
const timeClick = function(_e) {
|
||||||
const ts = entry_timestamp - start;
|
const ts = entry_timestamp - start;
|
||||||
if (ts > 0) {
|
if (ts > 0) {
|
||||||
props.jumpToTs(ts);
|
props.jumpToTs(ts);
|
||||||
|
|
@ -196,33 +144,38 @@ function LogElement(props) {
|
||||||
win.focus();
|
win.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
let className = 'cockpit-logline';
|
const cells = <DataListItemCells
|
||||||
if (start < entry_timestamp && end > entry_timestamp) {
|
dataListCells={[
|
||||||
className = 'cockpit-logline highlighted';
|
<DataListCell key="row">
|
||||||
}
|
<ExclamationTriangleIcon />
|
||||||
|
<Button variant="link" onClick={timeClick}>
|
||||||
|
{formatDateTime(entry_timestamp)}
|
||||||
|
</Button>
|
||||||
|
<Card isSelectable onClick={messageClick}>
|
||||||
|
<CardBody>{entry.MESSAGE}</CardBody>
|
||||||
|
</Card>
|
||||||
|
</DataListCell>
|
||||||
|
]} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} data-cursor={cursor} key={cursor}>
|
<DataListItem>
|
||||||
<div className="cockpit-log-warning">
|
<DataListItemRow>{cells}</DataListItemRow>
|
||||||
<i className="fa fa-exclamation-triangle" />
|
</DataListItem>
|
||||||
</div>
|
|
||||||
<div className="logs-view-log-time" onClick={timeClick}>{formatDateTime(entry_timestamp)}</div>
|
|
||||||
<span className="cockpit-log-message" onClick={messageClick}>{entry.MESSAGE}</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogsView(props) {
|
function LogsView(props) {
|
||||||
const entries = props.entries;
|
const { entries, start, end } = props;
|
||||||
const start = props.start;
|
|
||||||
const end = props.end;
|
|
||||||
const rows = entries.map((entry) =>
|
const rows = entries.map((entry) =>
|
||||||
<LogElement key={entry.__CURSOR} entry={entry} start={start} end={end} jumpToTs={props.jumpToTs} />
|
<LogElement
|
||||||
|
key={entry.__CURSOR}
|
||||||
|
entry={entry}
|
||||||
|
start={start}
|
||||||
|
end={end}
|
||||||
|
jumpToTs={props.jumpToTs} />
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="panel panel-default cockpit-log-panel" id="logs-view">
|
<DataList>{rows}</DataList>
|
||||||
{rows}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,16 +212,6 @@ class Logs extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToTop() {
|
|
||||||
const logs_view = document.getElementById("logs-view");
|
|
||||||
logs_view.scrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
const logs_view = document.getElementById("logs-view");
|
|
||||||
logs_view.scrollTop = logs_view.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
journalctlError(error) {
|
journalctlError(error) {
|
||||||
console.warn(cockpit.message(error));
|
console.warn(cockpit.message(error));
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +221,6 @@ class Logs extends React.Component {
|
||||||
this.entries.push(...entryList);
|
this.entries.push(...entryList);
|
||||||
const after = this.entries[this.entries.length - 1].__CURSOR;
|
const after = this.entries[this.entries.length - 1].__CURSOR;
|
||||||
this.setState({ entries: this.entries, after: after });
|
this.setState({ entries: this.entries, after: after });
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,7 +236,7 @@ class Logs extends React.Component {
|
||||||
this.journalCtl = null;
|
this.journalCtl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let matches = [];
|
const matches = [];
|
||||||
if (this.hostname) {
|
if (this.hostname) {
|
||||||
matches.push("_HOSTNAME=" + this.hostname);
|
matches.push("_HOSTNAME=" + this.hostname);
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +252,7 @@ class Logs extends React.Component {
|
||||||
end = formatDateTime(this.end);
|
end = formatDateTime(this.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = {
|
const options = {
|
||||||
since: start,
|
since: start,
|
||||||
until: end,
|
until: end,
|
||||||
follow: false,
|
follow: false,
|
||||||
|
|
@ -319,7 +261,7 @@ class Logs extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.after != null) {
|
if (this.state.after != null) {
|
||||||
options["after"] = this.state.after;
|
options.after = this.state.after;
|
||||||
delete options.since;
|
delete options.since;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,20 +317,36 @@ class Logs extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let r = this.props.recording;
|
const r = this.props.recording;
|
||||||
if (r == null) {
|
if (r == null) {
|
||||||
return <span>Loading...</span>;
|
return (
|
||||||
|
<Bullseye>
|
||||||
|
<EmptyState variant={EmptyStateVariant.small}>
|
||||||
|
<Spinner />
|
||||||
|
<Title headingLevel="h2" size="lg">
|
||||||
|
{_("Loading...")}
|
||||||
|
</Title>
|
||||||
|
</EmptyState>
|
||||||
|
</Bullseye>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="panel panel-default">
|
<>
|
||||||
<div className="panel-heading">
|
<LogsView
|
||||||
<span>{_("Logs")}</span>
|
id="logs-view"
|
||||||
<button className="btn btn-default" style={{"float":"right"}} onClick={this.loadLater}>{_("Load later entries")}</button>
|
entries={this.state.entries}
|
||||||
</div>
|
start={this.props.recording.start}
|
||||||
<LogsView entries={this.state.entries} start={this.props.recording.start}
|
end={this.props.recording.end}
|
||||||
end={this.props.recording.end} jumpToTs={this.props.jumpToTs} />
|
jumpToTs={this.props.jumpToTs} />
|
||||||
<div className="panel-heading" />
|
<Bullseye>
|
||||||
</div>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
onClick={this.loadLater}>
|
||||||
|
{_("Load later entries")}
|
||||||
|
</Button>
|
||||||
|
</Bullseye>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -445,12 +403,25 @@ class Recording extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let r = this.props.recording;
|
const r = this.props.recording;
|
||||||
if (r == null) {
|
if (r == null) {
|
||||||
return <span>Loading...</span>;
|
return (
|
||||||
|
<Bullseye>
|
||||||
|
<EmptyState variant={EmptyStateVariant.small}>
|
||||||
|
<Spinner />
|
||||||
|
<Title headingLevel="h2" size="lg">
|
||||||
|
{_("Loading...")}
|
||||||
|
</Title>
|
||||||
|
</EmptyState>
|
||||||
|
</Bullseye>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
let player =
|
return (
|
||||||
(<Player.Player
|
<>
|
||||||
|
<Button variant="link" icon={<AngleLeftIcon />} onClick={this.goBackToList}>
|
||||||
|
{_("Session Recording")}
|
||||||
|
</Button>
|
||||||
|
<Player.Player
|
||||||
ref="player"
|
ref="player"
|
||||||
matchList={this.props.recording.matchList}
|
matchList={this.props.recording.matchList}
|
||||||
logsTs={this.logsTs}
|
logsTs={this.logsTs}
|
||||||
|
|
@ -458,34 +429,18 @@ class Recording extends React.Component {
|
||||||
onTsChange={this.handleTsChange}
|
onTsChange={this.handleTsChange}
|
||||||
recording={r}
|
recording={r}
|
||||||
logsEnabled={this.state.logsEnabled}
|
logsEnabled={this.state.logsEnabled}
|
||||||
onRewindStart={this.handleLogsReset} />);
|
onRewindStart={this.handleLogsReset} />
|
||||||
|
<ExpandableSection
|
||||||
return (
|
id="btn-logs-view"
|
||||||
<React.Fragment>
|
toggleText={_("Logs View")}
|
||||||
<div className="container-fluid">
|
onToggle={this.handleLogsClick}
|
||||||
<div className="row">
|
isExpanded={this.state.logsEnabled === true}>
|
||||||
<div className="col-md-12">
|
<Logs
|
||||||
<ol className="breadcrumb">
|
recording={this.props.recording}
|
||||||
<li><a onClick={this.goBackToList}>{_("Session Recording")}</a></li>
|
curTs={this.state.curTs}
|
||||||
<li className="active">{_("Session")}</li>
|
jumpToTs={this.handleLogTsChange} />
|
||||||
</ol>
|
</ExpandableSection>
|
||||||
</div>
|
</>
|
||||||
{player}
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-12">
|
|
||||||
<button id="btn-logs-view" className="btn btn-default" style={{"float":"left"}} onClick={this.handleLogsClick}>{_("Logs View")}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.state.logsEnabled === true &&
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-12">
|
|
||||||
<Logs recording={this.props.recording} curTs={this.state.curTs} jumpToTs={this.handleLogTsChange} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -499,119 +454,82 @@ class Recording extends React.Component {
|
||||||
class RecordingList extends React.Component {
|
class RecordingList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleColumnClick = this.handleColumnClick.bind(this);
|
|
||||||
this.getSortedList = this.getSortedList.bind(this);
|
this.onSort = this.onSort.bind(this);
|
||||||
this.drawSortDir = this.drawSortDir.bind(this);
|
this.rowClickHandler = this.rowClickHandler.bind(this);
|
||||||
this.getColumnTitles = this.getColumnTitles.bind(this);
|
|
||||||
this.getColumns = this.getColumns.bind(this);
|
|
||||||
this.state = {
|
this.state = {
|
||||||
sorting_field: "start",
|
sortBy: {
|
||||||
sorting_asc: true,
|
index: 1,
|
||||||
|
direction: SortByDirection.asc
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
drawSortDir() {
|
onSort(_event, index, direction) {
|
||||||
$('#sort_arrow').remove();
|
|
||||||
let type = this.state.sorting_asc ? "asc" : "desc";
|
|
||||||
let arrow = '<i id="sort_arrow" class="fa fa-sort-' + type + '" aria-hidden="true" />';
|
|
||||||
$(this.refs[this.state.sorting_field]).append(arrow);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleColumnClick(event) {
|
|
||||||
if (this.state.sorting_field === event.currentTarget.id) {
|
|
||||||
this.setState({sorting_asc: !this.state.sorting_asc});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
sorting_field: event.currentTarget.id,
|
sortBy: {
|
||||||
sorting_asc: true,
|
index,
|
||||||
|
direction
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
getSortedList() {
|
rowClickHandler(_event, row) {
|
||||||
let field = this.state.sorting_field;
|
cockpit.location.go([row.id], cockpit.location.options);
|
||||||
let asc = this.state.sorting_asc;
|
|
||||||
let list = this.props.list.slice();
|
|
||||||
let isNumeric;
|
|
||||||
|
|
||||||
if (field === "start" || field === "end" || field === "duration") {
|
|
||||||
isNumeric = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNumeric) {
|
|
||||||
list.sort((a, b) => a[field] - b[field]);
|
|
||||||
} else {
|
|
||||||
list.sort((a, b) => (a[field] > b[field]) ? 1 : -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!asc) {
|
|
||||||
list.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Set the cockpit location to point to the specified recording.
|
|
||||||
*/
|
|
||||||
navigateToRecording(recording) {
|
|
||||||
cockpit.location.go([recording.id], cockpit.location.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.drawSortDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnTitles() {
|
|
||||||
let columnTitles = [
|
|
||||||
(<div id="user" className="sort" onClick={this.handleColumnClick}><span>{_("User")}</span> <div
|
|
||||||
ref="user" className="sort-icon" /></div>),
|
|
||||||
(<div id="start" className="sort" onClick={this.handleColumnClick}><span>{_("Start")}</span> <div
|
|
||||||
ref="start" className="sort-icon" /></div>),
|
|
||||||
(<div id="end" className="sort" onClick={this.handleColumnClick}><span>{_("End")}</span> <div
|
|
||||||
ref="end" className="sort-icon" /></div>),
|
|
||||||
(<div id="duration" className="sort" onClick={this.handleColumnClick}><span>{_("Duration")}</span> <div
|
|
||||||
ref="duration" className="sort-icon" /></div>),
|
|
||||||
];
|
|
||||||
if (this.props.diff_hosts === true) {
|
|
||||||
columnTitles.push((<div id="hostname" className="sort" onClick={this.handleColumnClick}>
|
|
||||||
<span>{_("Hostname")}</span> <div ref="hostname" className="sort-icon" /></div>));
|
|
||||||
}
|
|
||||||
return columnTitles;
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumns(r) {
|
|
||||||
let columns = [r.user,
|
|
||||||
formatDateTime(r.start),
|
|
||||||
formatDateTime(r.end),
|
|
||||||
formatDuration(r.end - r.start)];
|
|
||||||
if (this.props.diff_hosts === true) {
|
|
||||||
columns.push(r.hostname);
|
|
||||||
}
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let columnTitles = this.getColumnTitles();
|
const { sortBy } = this.state;
|
||||||
let list = this.getSortedList();
|
const { index, direction } = sortBy;
|
||||||
let rows = [];
|
|
||||||
|
// generate columns
|
||||||
|
let titles = ["User", "Start", "End", "Duration"];
|
||||||
|
if (this.props.diff_hosts === true)
|
||||||
|
titles.push("Hostname");
|
||||||
|
const columnTitles = titles.map(title => ({
|
||||||
|
title: _(title),
|
||||||
|
transforms: [sortable]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// sort rows
|
||||||
|
let rows = this.props.list.map(rec => {
|
||||||
|
let cells = [
|
||||||
|
rec.user,
|
||||||
|
formatDateTime(rec.start),
|
||||||
|
formatDateTime(rec.end),
|
||||||
|
formatDuration(rec.end - rec.start),
|
||||||
|
];
|
||||||
|
if (this.props.diff_hosts === true)
|
||||||
|
cells.push(rec.hostname);
|
||||||
|
return {
|
||||||
|
id: rec.id,
|
||||||
|
cells: cells
|
||||||
|
};
|
||||||
|
}).sort((a, b) => a.cells[index].localeCompare(b.cells[index]));
|
||||||
|
rows = direction === SortByDirection.asc ? rows : rows.reverse();
|
||||||
|
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
let r = list[i];
|
|
||||||
let columns = this.getColumns(r);
|
|
||||||
rows.push(<Listing.ListingRow
|
|
||||||
key={r.id}
|
|
||||||
rowId={r.id}
|
|
||||||
columns={columns}
|
|
||||||
navigateToItem={this.navigateToRecording.bind(this, r)} />);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Listing.Listing title={_("Sessions")}
|
<>
|
||||||
columnTitles={columnTitles}
|
<Table
|
||||||
emptyCaption={_("No recorded sessions")}
|
aria-label={_("Recordings")}
|
||||||
fullWidth={false}>
|
cells={columnTitles}
|
||||||
{rows}
|
rows={rows}
|
||||||
</Listing.Listing>
|
sortBy={sortBy}
|
||||||
|
onSort={this.onSort}>
|
||||||
|
<TableHeader />
|
||||||
|
<TableBody onRowClick={this.rowClickHandler} />
|
||||||
|
</Table>
|
||||||
|
{!rows.length &&
|
||||||
|
<EmptyState variant={EmptyStateVariant.small}>
|
||||||
|
<EmptyStateIcon icon={SearchIcon} />
|
||||||
|
<Title headingLevel="h2" size="lg">
|
||||||
|
{_("No recordings found")}
|
||||||
|
</Title>
|
||||||
|
<EmptyStateBody>
|
||||||
|
{_("No recordings matched the filter criteria.")}
|
||||||
|
</EmptyStateBody>
|
||||||
|
</EmptyState>}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -621,13 +539,12 @@ class RecordingList extends React.Component {
|
||||||
* single recording. Extracts the ID of the recording to display from
|
* single recording. Extracts the ID of the recording to display from
|
||||||
* cockpit.location.path[0]. If it's zero, displays the list.
|
* cockpit.location.path[0]. If it's zero, displays the list.
|
||||||
*/
|
*/
|
||||||
class View extends React.Component {
|
export default class View extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onLocationChanged = this.onLocationChanged.bind(this);
|
this.onLocationChanged = this.onLocationChanged.bind(this);
|
||||||
this.journalctlIngest = this.journalctlIngest.bind(this);
|
this.journalctlIngest = this.journalctlIngest.bind(this);
|
||||||
this.handleInputChange = this.handleInputChange.bind(this);
|
this.handleInputChange = this.handleInputChange.bind(this);
|
||||||
this.handleDateSinceChange = this.handleDateSinceChange.bind(this);
|
|
||||||
this.openConfig = this.openConfig.bind(this);
|
this.openConfig = this.openConfig.bind(this);
|
||||||
/* Journalctl instance */
|
/* Journalctl instance */
|
||||||
this.journalctl = null;
|
this.journalctl = null;
|
||||||
|
|
@ -637,11 +554,12 @@ class View extends React.Component {
|
||||||
this.recordingMap = {};
|
this.recordingMap = {};
|
||||||
/* tlog UID in system set in ComponentDidMount */
|
/* tlog UID in system set in ComponentDidMount */
|
||||||
this.uid = null;
|
this.uid = null;
|
||||||
|
const path = cockpit.location.path[0];
|
||||||
this.state = {
|
this.state = {
|
||||||
/* List of recordings in start order */
|
/* List of recordings in start order */
|
||||||
recordingList: [],
|
recordingList: [],
|
||||||
/* ID of the recording to display, or null for all */
|
/* ID of the recording to display, or null for all */
|
||||||
recordingID: cockpit.location.path[0] || null,
|
recordingID: path === "config" ? null : path || null,
|
||||||
/* filter values start */
|
/* filter values start */
|
||||||
date_since: cockpit.location.options.date_since || "",
|
date_since: cockpit.location.options.date_since || "",
|
||||||
date_until: cockpit.location.options.date_until || "",
|
date_until: cockpit.location.options.date_until || "",
|
||||||
|
|
@ -651,6 +569,8 @@ class View extends React.Component {
|
||||||
/* filter values end */
|
/* filter values end */
|
||||||
error_tlog_uid: false,
|
error_tlog_uid: false,
|
||||||
diff_hosts: false,
|
diff_hosts: false,
|
||||||
|
/* if config is open */
|
||||||
|
config: path === "config",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -666,6 +586,10 @@ class View extends React.Component {
|
||||||
* displayed recording ID.
|
* displayed recording ID.
|
||||||
*/
|
*/
|
||||||
onLocationChanged() {
|
onLocationChanged() {
|
||||||
|
const path = cockpit.location.path[0];
|
||||||
|
if (path === "config")
|
||||||
|
this.setState({ config: true });
|
||||||
|
else
|
||||||
this.setState({
|
this.setState({
|
||||||
recordingID: cockpit.location.path[0] || null,
|
recordingID: cockpit.location.path[0] || null,
|
||||||
date_since: cockpit.location.options.date_since || "",
|
date_since: cockpit.location.options.date_since || "",
|
||||||
|
|
@ -673,6 +597,7 @@ class View extends React.Component {
|
||||||
username: cockpit.location.options.username || "",
|
username: cockpit.location.options.username || "",
|
||||||
hostname: cockpit.location.options.hostname || "",
|
hostname: cockpit.location.options.hostname || "",
|
||||||
search: cockpit.location.options.search || "",
|
search: cockpit.location.options.search || "",
|
||||||
|
config: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -680,49 +605,51 @@ class View extends React.Component {
|
||||||
* Ingest journal entries sent by journalctl.
|
* Ingest journal entries sent by journalctl.
|
||||||
*/
|
*/
|
||||||
journalctlIngest(entryList) {
|
journalctlIngest(entryList) {
|
||||||
let recordingList = this.state.recordingList.slice();
|
const recordingList = this.state.recordingList.slice();
|
||||||
let i;
|
let i;
|
||||||
let j;
|
let j;
|
||||||
let hostname;
|
let hostname;
|
||||||
|
|
||||||
if (entryList[0]) {
|
if (entryList[0]) {
|
||||||
if (entryList[0]["_HOSTNAME"]) {
|
if (entryList[0]._HOSTNAME) {
|
||||||
hostname = entryList[0]["_HOSTNAME"];
|
hostname = entryList[0]._HOSTNAME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < entryList.length; i++) {
|
for (i = 0; i < entryList.length; i++) {
|
||||||
let e = entryList[i];
|
const e = entryList[i];
|
||||||
let id = e['TLOG_REC'];
|
const id = e.TLOG_REC;
|
||||||
|
|
||||||
/* Skip entries with missing recording ID */
|
/* Skip entries with missing recording ID */
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ts = Math.floor(
|
const ts = Math.floor(
|
||||||
parseInt(e["__REALTIME_TIMESTAMP"], 10) /
|
parseInt(e.__REALTIME_TIMESTAMP, 10) /
|
||||||
1000);
|
1000);
|
||||||
|
|
||||||
let r = this.recordingMap[id];
|
let r = this.recordingMap[id];
|
||||||
/* If no recording found */
|
/* If no recording found */
|
||||||
if (r === undefined) {
|
if (r === undefined) {
|
||||||
/* Create new recording */
|
/* Create new recording */
|
||||||
if (hostname !== e["_HOSTNAME"]) {
|
if (hostname !== e._HOSTNAME) {
|
||||||
this.setState({ diff_hosts: true });
|
this.setState({ diff_hosts: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
r = {id: id,
|
r = {
|
||||||
|
id: id,
|
||||||
matchList: ["TLOG_REC=" + id],
|
matchList: ["TLOG_REC=" + id],
|
||||||
user: e["TLOG_USER"],
|
user: e.TLOG_USER,
|
||||||
boot_id: e["_BOOT_ID"],
|
boot_id: e._BOOT_ID,
|
||||||
session_id: parseInt(e["TLOG_SESSION"], 10),
|
session_id: parseInt(e.TLOG_SESSION, 10),
|
||||||
pid: parseInt(e["_PID"], 10),
|
pid: parseInt(e._PID, 10),
|
||||||
start: ts,
|
start: ts,
|
||||||
/* FIXME Should be start + message duration */
|
/* FIXME Should be start + message duration */
|
||||||
end: ts,
|
end: ts,
|
||||||
hostname: e["_HOSTNAME"],
|
hostname: e._HOSTNAME,
|
||||||
duration: 0};
|
duration: 0
|
||||||
|
};
|
||||||
/* Map the recording */
|
/* Map the recording */
|
||||||
this.recordingMap[id] = r;
|
this.recordingMap[id] = r;
|
||||||
/* Insert the recording in order */
|
/* Insert the recording in order */
|
||||||
|
|
@ -765,7 +692,7 @@ class View extends React.Component {
|
||||||
* Assumes journalctl is not running.
|
* Assumes journalctl is not running.
|
||||||
*/
|
*/
|
||||||
journalctlStart() {
|
journalctlStart() {
|
||||||
let matches = ["_COMM=tlog-rec",
|
const matches = ["_COMM=tlog-rec",
|
||||||
/* Strings longer than TASK_COMM_LEN (16) characters
|
/* Strings longer than TASK_COMM_LEN (16) characters
|
||||||
* are truncated (man proc) */
|
* are truncated (man proc) */
|
||||||
"_COMM=tlog-rec-sessio"];
|
"_COMM=tlog-rec-sessio"];
|
||||||
|
|
@ -777,22 +704,22 @@ class View extends React.Component {
|
||||||
matches.push("_HOSTNAME=" + this.state.hostname);
|
matches.push("_HOSTNAME=" + this.state.hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = {follow: false, count: "all", merge: true};
|
const options = { follow: false, count: "all", merge: true };
|
||||||
|
|
||||||
if (this.state.date_since && this.state.date_since !== "") {
|
if (this.state.date_since && this.state.date_since !== "") {
|
||||||
options['since'] = formatUTC(this.state.date_since);
|
options.since = formatUTC(this.state.date_since);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.date_until && this.state.date_until !== "") {
|
if (this.state.date_until && this.state.date_until !== "") {
|
||||||
options['until'] = formatUTC(this.state.date_until);
|
options.until = formatUTC(this.state.date_until);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.search && this.state.search !== "" && this.state.recordingID === null) {
|
if (this.state.search && this.state.search !== "" && this.state.recordingID === null) {
|
||||||
options["grep"] = this.state.search;
|
options.grep = this.state.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.recordingID !== null) {
|
if (this.state.recordingID !== null) {
|
||||||
delete options["grep"];
|
delete options.grep;
|
||||||
matches.push("TLOG_REC=" + this.state.recordingID);
|
matches.push("TLOG_REC=" + this.state.recordingID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -838,29 +765,19 @@ class View extends React.Component {
|
||||||
this.setState({ recordingList: [] });
|
this.setState({ recordingList: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange(event) {
|
handleInputChange(name, value) {
|
||||||
const name = event.target.name;
|
const state = {};
|
||||||
const value = event.target.value;
|
|
||||||
let state = {};
|
|
||||||
state[name] = value;
|
state[name] = value;
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
cockpit.location.go([], $.extend(cockpit.location.options, state));
|
cockpit.location.go([], $.extend(cockpit.location.options, state));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDateSinceChange(date) {
|
|
||||||
cockpit.location.go([], $.extend(cockpit.location.options, {date_since: date}));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDateUntilChange(date) {
|
|
||||||
cockpit.location.go([], $.extend(cockpit.location.options, {date_until: date}));
|
|
||||||
}
|
|
||||||
|
|
||||||
openConfig() {
|
openConfig() {
|
||||||
cockpit.jump(['session-recording/config']);
|
cockpit.location.go("/config");
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
let proc = cockpit.spawn(["getent", "passwd", "tlog"]);
|
const proc = cockpit.spawn(["getent", "passwd", "tlog"]);
|
||||||
|
|
||||||
proc.stream((data) => {
|
proc.stream((data) => {
|
||||||
this.uid = data.split(":", 3)[2];
|
this.uid = data.split(":", 3)[2];
|
||||||
|
|
@ -882,7 +799,7 @@ class View extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(_prevProps, prevState) {
|
||||||
/*
|
/*
|
||||||
* If we're running a specific (non-wildcard) journalctl
|
* If we're running a specific (non-wildcard) journalctl
|
||||||
* and recording ID has changed
|
* and recording ID has changed
|
||||||
|
|
@ -906,75 +823,94 @@ class View extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.error_tlog_uid === true) {
|
if (this.state.config === true) {
|
||||||
|
return <Config.Config />;
|
||||||
|
} else if (this.state.error_tlog_uid === true) {
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid">
|
<Bullseye>
|
||||||
Error getting tlog UID from system.
|
<EmptyState variant={EmptyStateVariant.small}>
|
||||||
</div>
|
<EmptyStateIcon
|
||||||
|
icon={ExclamationCircleIcon}
|
||||||
|
color={global_danger_color_200.value} />
|
||||||
|
<Title headingLevel="h2" size="lg">
|
||||||
|
{_("Error")}
|
||||||
|
</Title>
|
||||||
|
<EmptyStateBody>
|
||||||
|
{_("Unable to retrieve tlog UID from system.")}
|
||||||
|
</EmptyStateBody>
|
||||||
|
</EmptyState>
|
||||||
|
</Bullseye>
|
||||||
);
|
);
|
||||||
}
|
} else if (this.state.recordingID === null) {
|
||||||
if (this.state.recordingID === null) {
|
const toolbar = (
|
||||||
|
<ToolbarContent>
|
||||||
|
<ToolbarGroup>
|
||||||
|
<ToolbarItem variant="label">{_("Since")}</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<TextInput
|
||||||
|
id="filter-since"
|
||||||
|
placeholder={_("Filter since")}
|
||||||
|
value={this.state.date_since}
|
||||||
|
type="search"
|
||||||
|
onChange={value => this.handleInputChange("date_since", value)} />
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>
|
||||||
|
<ToolbarGroup>
|
||||||
|
<ToolbarItem variant="label">{_("Until")}</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<TextInput
|
||||||
|
id="filter-until"
|
||||||
|
placeholder={_("Filter until")}
|
||||||
|
value={this.state.date_until}
|
||||||
|
type="search"
|
||||||
|
onChange={value => this.handleInputChange("date_until", value)} />
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>
|
||||||
|
<ToolbarGroup>
|
||||||
|
<ToolbarItem variant="label">{_("Search")}</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<TextInput
|
||||||
|
id="filter-search"
|
||||||
|
placeholder={_("Filter by content")}
|
||||||
|
value={this.state.search}
|
||||||
|
type="search"
|
||||||
|
onChange={value => this.handleInputChange("search", value)} />
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>
|
||||||
|
<ToolbarGroup>
|
||||||
|
<ToolbarItem variant="label">{_("Username")}</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<TextInput
|
||||||
|
id="filter-username"
|
||||||
|
placeholder={_("Filter by username")}
|
||||||
|
value={this.state.username}
|
||||||
|
type="search"
|
||||||
|
onChange={value => this.handleInputChange("username", value)} />
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>
|
||||||
|
{this.state.diff_hosts === true &&
|
||||||
|
<ToolbarGroup>
|
||||||
|
<ToolbarItem variant="label">{_("Hostname")}</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<TextInput
|
||||||
|
id="filter-hostname"
|
||||||
|
placeholder={_("Filter by hostname")}
|
||||||
|
value={this.state.hostname}
|
||||||
|
type="search"
|
||||||
|
onChange={value => this.handleInputChange("hostname", value)} />
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>}
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button id="btn-config" onClick={this.openConfig}>
|
||||||
|
<CogIcon />
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarContent>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
<div className="content-header-extra">
|
<Toolbar>{toolbar}</Toolbar>
|
||||||
<table className="form-table-ct">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td className="top">
|
|
||||||
<label className="control-label" htmlFor="date_since">{_("Since")}</label>
|
|
||||||
</td>
|
|
||||||
<td id="since-search">
|
|
||||||
<Datetimepicker value={this.state.date_since} onChange={this.handleDateSinceChange} />
|
|
||||||
</td>
|
|
||||||
<td className="top">
|
|
||||||
<label className="control-label" htmlFor="date_until">{_("Until")}</label>
|
|
||||||
</td>
|
|
||||||
<td id="until-search">
|
|
||||||
<Datetimepicker value={this.state.date_until} onChange={this.handleDateUntilChange} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="top">
|
|
||||||
<label className="control-label" htmlFor="search">Search</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="input-group">
|
|
||||||
<input type="text" id="recording-search" className="form-control" name="search" value={this.state.search}
|
|
||||||
onChange={this.handleInputChange} />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="top">
|
|
||||||
<label className="control-label" htmlFor="username">Username</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="input-group">
|
|
||||||
<input type="text" id="username-search" className="form-control" name="username" value={this.state.username}
|
|
||||||
onChange={this.handleInputChange} />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{this.state.diff_hosts === true &&
|
|
||||||
<td className="top">
|
|
||||||
<label className="control-label" htmlFor="hostname">{_("Hostname")}</label>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
{this.state.diff_hosts === true &&
|
|
||||||
<td>
|
|
||||||
<div className="input-group">
|
|
||||||
<input type="text" className="form-control" name="hostname" value={this.state.hostname}
|
|
||||||
onChange={this.handleInputChange} />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
<td className="top">
|
|
||||||
<label className="control-label" htmlFor="config">{_("Configuration")}</label>
|
|
||||||
</td>
|
|
||||||
<td className="top">
|
|
||||||
<button id="btn-config" className="btn btn-default" onClick={this.openConfig}><i className="fa fa-cog" aria-hidden="true" /></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<RecordingList
|
<RecordingList
|
||||||
date_since={this.state.date_since}
|
date_since={this.state.date_since}
|
||||||
date_until={this.state.date_until}
|
date_until={this.state.date_until}
|
||||||
|
|
@ -982,16 +918,14 @@ class View extends React.Component {
|
||||||
hostname={this.state.hostname}
|
hostname={this.state.hostname}
|
||||||
list={this.state.recordingList}
|
list={this.state.recordingList}
|
||||||
diff_hosts={this.state.diff_hosts} />
|
diff_hosts={this.state.diff_hosts} />
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<Recording
|
||||||
<Recording recording={this.recordingMap[this.state.recordingID]} search={this.state.search} />
|
recording={this.recordingMap[this.state.recordingID]}
|
||||||
</React.Fragment>
|
search={this.state.search} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(<View />, document.getElementById('view'));
|
|
||||||
|
|
|
||||||
149
src/table.css
149
src/table.css
|
|
@ -1,149 +0,0 @@
|
||||||
/* Panels don't draw borders between them */
|
|
||||||
.panel > .table > tbody:first-child td {
|
|
||||||
border-top: 1px solid rgb(221, 221, 221);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table headers should not generate a double border */
|
|
||||||
.panel .table thead tr th {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-heading {
|
|
||||||
background: #F5F5F5;
|
|
||||||
height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vertically center dropdown buttons in panel headers */
|
|
||||||
.panel-heading .btn {
|
|
||||||
margin-top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Fix up table row hovering.
|
|
||||||
*
|
|
||||||
* When you hover over table rows it's because they're clickable.
|
|
||||||
* Make the table row hover color match the list-group-item.
|
|
||||||
*/
|
|
||||||
.table-hover > tbody > tr > td,
|
|
||||||
.table-hover > tbody > tr > th,
|
|
||||||
.dialog-list-ct .list-group-item {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.table-hover > tbody > tr:hover > td,
|
|
||||||
.table-hover > tbody > tr:hover > th,
|
|
||||||
.dialog-list-ct .list-group-item:hover:not(.active) {
|
|
||||||
background-color: #d4edfa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override patternfly to fit buttons and such */
|
|
||||||
.table > thead > tr > th,
|
|
||||||
.table > tbody > tr > td {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override the heavy patternfly headers */
|
|
||||||
.table > thead {
|
|
||||||
background-image: none;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make things line up */
|
|
||||||
.table tbody tr td:first-child,
|
|
||||||
.table thead tr th:first-child {
|
|
||||||
padding-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr td:last-child,
|
|
||||||
.table thead tr th:last-child {
|
|
||||||
padding-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table-ct > tr > td,
|
|
||||||
.info-table-ct > tbody > tr > td {
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding-left: 0.75em;
|
|
||||||
padding-top: 0.25em;
|
|
||||||
vertical-align: top;
|
|
||||||
line-height: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Match the form-table-ct CSS equivalent */
|
|
||||||
.info-table-ct > tr > td:first-child,
|
|
||||||
.info-table-ct > tbody > tr > td:first-child {
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
width: 5px;
|
|
||||||
color: #888888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table-ct > tr > td:not(:first-child),
|
|
||||||
.info-table-ct > tbody > tr > td:not(:first-child) {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table-ct > tr > td button,
|
|
||||||
.info-table-ct > tbody > tr > td button {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct td {
|
|
||||||
padding-left: 0.75em;
|
|
||||||
padding-top: 0.25em;
|
|
||||||
line-height: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct td.top {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct td:first-child {
|
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #888888;
|
|
||||||
width: 5px; /* will be expanded by nowrap */
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct td[colspan] {
|
|
||||||
text-align: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct td {
|
|
||||||
height: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct td.header {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: left;
|
|
||||||
color: #4D5258;
|
|
||||||
padding: 20px 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct label input[type='radio'],
|
|
||||||
.form-table-ct label input[type='checkbox'] {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct label {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-table-ct label span {
|
|
||||||
vertical-align: super;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Break up sidebar in columns in smaller sizes*/
|
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.info-table-ct-container .info-table-ct {
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,12 +18,16 @@ class TestApplication(MachineCase):
|
||||||
self.login_and_go("/session-recording")
|
self.login_and_go("/session-recording")
|
||||||
b = self.browser
|
b = self.browser
|
||||||
m = self.machine
|
m = self.machine
|
||||||
b.wait_present(".content-header-extra")
|
b.wait_present("#app")
|
||||||
b.wait_present("#user")
|
|
||||||
return b, m
|
return b, m
|
||||||
|
|
||||||
def _sel_rec(self, cond=":first()"):
|
def _sel_rec(self, index=0):
|
||||||
self.browser.click(f".listing-ct-item{cond}")
|
page = (
|
||||||
|
"0f25700a28c44b599869745e5fda8b0c-7106-121e79"
|
||||||
|
if not index
|
||||||
|
else "0f25700a28c44b599869745e5fda8b0c-7623-135541"
|
||||||
|
)
|
||||||
|
self.browser.go(f"/session-recording#/{page}")
|
||||||
|
|
||||||
def _term_line(self, lineno):
|
def _term_line(self, lineno):
|
||||||
return f".xterm-accessibility-tree div:nth-child({lineno})"
|
return f".xterm-accessibility-tree div:nth-child({lineno})"
|
||||||
|
|
@ -35,18 +39,18 @@ class TestApplication(MachineCase):
|
||||||
b.wait_in_text(self._term_line(1), "localhost")
|
b.wait_in_text(self._term_line(1), "localhost")
|
||||||
|
|
||||||
def testFastforwardControls(self):
|
def testFastforwardControls(self):
|
||||||
slider = ".slider > .min-slider-handle"
|
progress = ".pf-c-progress__indicator"
|
||||||
|
|
||||||
b, _ = self._login()
|
b, _ = self._login()
|
||||||
self._sel_rec()
|
self._sel_rec()
|
||||||
# fast forward
|
# fast forward
|
||||||
b.click("#player-fast-forward")
|
b.click("#player-fast-forward")
|
||||||
b.wait_in_text(self._term_line(12), "exit")
|
b.wait_in_text(self._term_line(12), "exit")
|
||||||
b.wait_attr(slider, "style", "left: 100%;")
|
b.wait_attr(progress, "style", "width: 100%;")
|
||||||
# test restart playback
|
# test restart playback
|
||||||
b.click("#player-restart")
|
b.click("#player-restart")
|
||||||
b.wait_text(self._term_line(1), "Blank line")
|
b.wait_text(self._term_line(1), "Blank line")
|
||||||
b.wait_attr(slider, "style", "left: 100%;")
|
b.wait_attr(progress, "style", "width: 100%;")
|
||||||
|
|
||||||
def testSpeedControls(self):
|
def testSpeedControls(self):
|
||||||
b, _ = self._login()
|
b, _ = self._login()
|
||||||
|
|
@ -54,7 +58,6 @@ class TestApplication(MachineCase):
|
||||||
# increase speed
|
# increase speed
|
||||||
b.wait_present("#player-speed-up")
|
b.wait_present("#player-speed-up")
|
||||||
b.click("#player-speed-up")
|
b.click("#player-speed-up")
|
||||||
b.wait_present("#player-speed")
|
|
||||||
b.wait_text("#player-speed", "x2")
|
b.wait_text("#player-speed", "x2")
|
||||||
b.click("#player-speed-up")
|
b.click("#player-speed-up")
|
||||||
b.wait_text("#player-speed", "x4")
|
b.wait_text("#player-speed", "x4")
|
||||||
|
|
@ -70,7 +73,6 @@ class TestApplication(MachineCase):
|
||||||
b.click("#player-speed-down")
|
b.click("#player-speed-down")
|
||||||
b.wait_text("#player-speed", "x2")
|
b.wait_text("#player-speed", "x2")
|
||||||
b.click("#player-speed-down")
|
b.click("#player-speed-down")
|
||||||
b.wait_present("#player-speed")
|
|
||||||
b.click("#player-speed-down")
|
b.click("#player-speed-down")
|
||||||
b.wait_text("#player-speed", "/2")
|
b.wait_text("#player-speed", "/2")
|
||||||
b.click("#player-speed-down")
|
b.click("#player-speed-down")
|
||||||
|
|
@ -80,8 +82,7 @@ class TestApplication(MachineCase):
|
||||||
b.click("#player-speed-down")
|
b.click("#player-speed-down")
|
||||||
b.wait_text("#player-speed", "/16")
|
b.wait_text("#player-speed", "/16")
|
||||||
# restore speed
|
# restore speed
|
||||||
b.click("#player-speed-reset")
|
b.click(".pf-c-chip .pf-c-button")
|
||||||
b.wait_present("#player-speed")
|
|
||||||
b.click("#player-speed-down")
|
b.click("#player-speed-down")
|
||||||
b.wait_text("#player-speed", "/2")
|
b.wait_text("#player-speed", "/2")
|
||||||
|
|
||||||
|
|
@ -142,9 +143,7 @@ class TestApplication(MachineCase):
|
||||||
import time, json, configparser
|
import time, json, configparser
|
||||||
|
|
||||||
b, m = self._login()
|
b, m = self._login()
|
||||||
# Ensure that the button leads to the config page
|
|
||||||
b.click("#btn-config")
|
b.click("#btn-config")
|
||||||
b.enter_page("/session-recording/config")
|
|
||||||
|
|
||||||
# TLOG config
|
# TLOG config
|
||||||
conf_file_path = "/etc/tlog/"
|
conf_file_path = "/etc/tlog/"
|
||||||
|
|
@ -179,7 +178,9 @@ class TestApplication(MachineCase):
|
||||||
m.upload([save_file], conf_file_path)
|
m.upload([save_file], conf_file_path)
|
||||||
# Check that the config reflects the changes
|
# Check that the config reflects the changes
|
||||||
conf = json.load(open(test_file, "r"))
|
conf = json.load(open(test_file, "r"))
|
||||||
assert json.dumps(conf) == json.dumps(
|
self.assertEqual(
|
||||||
|
json.dumps(conf),
|
||||||
|
json.dumps(
|
||||||
{
|
{
|
||||||
"shell": "/test/path/shell",
|
"shell": "/test/path/shell",
|
||||||
"notice": "Test Notice",
|
"notice": "Test Notice",
|
||||||
|
|
@ -192,6 +193,7 @@ class TestApplication(MachineCase):
|
||||||
"journal": {"priority": "info", "augment": False},
|
"journal": {"priority": "info", "augment": False},
|
||||||
"writer": "file",
|
"writer": "file",
|
||||||
}
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# SSSD config
|
# SSSD config
|
||||||
|
|
@ -228,13 +230,13 @@ class TestApplication(MachineCase):
|
||||||
# Check that the configs reflected the changes
|
# Check that the configs reflected the changes
|
||||||
conf = configparser.ConfigParser()
|
conf = configparser.ConfigParser()
|
||||||
conf.read_file(open(test_none_file, "r"))
|
conf.read_file(open(test_none_file, "r"))
|
||||||
assert conf["session_recording"]["scope"] == "none"
|
self.assertEqual(conf["session_recording"]["scope"], "none")
|
||||||
conf.read_file(open(test_some_file, "r"))
|
conf.read_file(open(test_some_file, "r"))
|
||||||
assert conf["session_recording"]["scope"] == "some"
|
self.assertEqual(conf["session_recording"]["scope"], "some")
|
||||||
assert conf["session_recording"]["users"] == "test users"
|
self.assertEqual(conf["session_recording"]["users"], "test users")
|
||||||
assert conf["session_recording"]["groups"] == "test groups"
|
self.assertEqual(conf["session_recording"]["groups"], "test groups")
|
||||||
conf.read_file(open(test_all_file, "r"))
|
conf.read_file(open(test_all_file, "r"))
|
||||||
assert conf["session_recording"]["scope"] == "all"
|
self.assertEqual(conf["session_recording"]["scope"], "all")
|
||||||
|
|
||||||
def testDisplayDrag(self):
|
def testDisplayDrag(self):
|
||||||
b, _ = self._login()
|
b, _ = self._login()
|
||||||
|
|
@ -248,23 +250,22 @@ class TestApplication(MachineCase):
|
||||||
b.click("#player-zoom-in")
|
b.click("#player-zoom-in")
|
||||||
# select and ensure drag'n'pan mode
|
# select and ensure drag'n'pan mode
|
||||||
b.click("#player-drag-pan")
|
b.click("#player-drag-pan")
|
||||||
b.wait_present(".fa-hand-rock-o")
|
|
||||||
# scroll and check for screen movement
|
# scroll and check for screen movement
|
||||||
b.mouse(".dragnpan", "mousedown", 200, 200)
|
b.mouse(".dragnpan", "mousedown", 200, 200)
|
||||||
b.mouse(".dragnpan", "mousemove", 10, 10)
|
b.mouse(".dragnpan", "mousemove", 10, 10)
|
||||||
assert b.attr(".dragnpan", "scrollTop") != 0
|
self.assertNotEqual(b.attr(".dragnpan", "scrollTop"), 0)
|
||||||
assert b.attr(".dragnpan", "scrollLeft") != 0
|
self.assertNotEqual(b.attr(".dragnpan", "scrollLeft"), 0)
|
||||||
|
|
||||||
def testLogCorrelation(self):
|
def testLogCorrelation(self):
|
||||||
b, _ = self._login()
|
b, _ = self._login()
|
||||||
# select the recording with the extra logs
|
# select the recording with the extra logs
|
||||||
self._sel_rec(":contains('01:07')")
|
self._sel_rec(1)
|
||||||
b.click("#btn-logs-view")
|
b.click("#btn-logs-view")
|
||||||
# fast forward until the end
|
# fast forward until the end
|
||||||
while "exit" not in b.text(self._term_line(22)):
|
while "exit" not in b.text(self._term_line(22)):
|
||||||
b.click("#player-skip-frame")
|
b.click("#player-skip-frame")
|
||||||
# check for extra log entries
|
# check for extra log entries
|
||||||
b.wait_present(".cockpit-log-message:contains('authentication failure')")
|
b.wait_present(".pf-c-data-list:contains('authentication failure')")
|
||||||
|
|
||||||
def testZoomSpeedControls(self):
|
def testZoomSpeedControls(self):
|
||||||
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
|
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
|
||||||
|
|
@ -290,17 +291,20 @@ class TestApplication(MachineCase):
|
||||||
def _filter(self, inp, occ_dict):
|
def _filter(self, inp, occ_dict):
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# allow temporary timestamp failures
|
||||||
|
self.allow_journal_messages(".*timestamp.*")
|
||||||
|
# login and test inputs
|
||||||
b, _ = self._login()
|
b, _ = self._login()
|
||||||
for occ in occ_dict:
|
for occ in occ_dict:
|
||||||
for term in occ_dict[occ]:
|
for term in occ_dict[occ]:
|
||||||
# enter the search term and wait for the results to return
|
# enter the search term and wait for the results to return
|
||||||
b.set_input_text(inp, term)
|
b.set_input_text(inp, term)
|
||||||
time.sleep(0.5)
|
time.sleep(2)
|
||||||
assert b.text(".listing-ct").count("contractor") == occ
|
self.assertEqual(b.text(".pf-c-table").count("contractor"), occ)
|
||||||
|
|
||||||
def testSearch(self):
|
def testSearch(self):
|
||||||
self._filter(
|
self._filter(
|
||||||
"#recording-search",
|
"#filter-search",
|
||||||
{
|
{
|
||||||
0: {
|
0: {
|
||||||
"this should return nothing",
|
"this should return nothing",
|
||||||
|
|
@ -327,7 +331,7 @@ class TestApplication(MachineCase):
|
||||||
|
|
||||||
def testFilterUsername(self):
|
def testFilterUsername(self):
|
||||||
self._filter(
|
self._filter(
|
||||||
"#username-search",
|
"#filter-username",
|
||||||
{
|
{
|
||||||
0: {"test", "contact", "contractor", "contractor11", "contractor4"},
|
0: {"test", "contact", "contractor", "contractor11", "contractor4"},
|
||||||
2: {"contractor1"},
|
2: {"contractor1"},
|
||||||
|
|
@ -336,7 +340,7 @@ class TestApplication(MachineCase):
|
||||||
|
|
||||||
def testFilterSince(self):
|
def testFilterSince(self):
|
||||||
self._filter(
|
self._filter(
|
||||||
"#since-search .bootstrap-datepicker",
|
"#filter-since",
|
||||||
{
|
{
|
||||||
0: {"2020-06-02", "2020-06-01 12:31:00"},
|
0: {"2020-06-02", "2020-06-01 12:31:00"},
|
||||||
1: {"2020-06-01 12:17:01", "2020-06-01 12:30:50"},
|
1: {"2020-06-01 12:17:01", "2020-06-01 12:30:50"},
|
||||||
|
|
@ -346,10 +350,10 @@ class TestApplication(MachineCase):
|
||||||
|
|
||||||
def testFilterUntil(self):
|
def testFilterUntil(self):
|
||||||
self._filter(
|
self._filter(
|
||||||
"#until-search .bootstrap-datepicker",
|
"#filter-until",
|
||||||
{
|
{
|
||||||
0: {"2020-06-01", "2020-06-01 12:16:00"},
|
0: {"2020-06-01", "2020-06-01 12:16"},
|
||||||
1: {"2020-06-01 12:17:00", "2020-06-01 12:29:42"},
|
1: {"2020-06-01 12:17", "2020-06-01 12:29"},
|
||||||
2: {"2020-06-02", "2020-06-01 12:31:00"},
|
2: {"2020-06-02", "2020-06-01 12:31:00"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,34 @@
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const copy = require("copy-webpack-plugin");
|
const copy = require("copy-webpack-plugin");
|
||||||
const extract = require("extract-text-webpack-plugin");
|
const extract = require("mini-css-extract-plugin");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const CompressionPlugin = require("compression-webpack-plugin");
|
const CompressionPlugin = require("compression-webpack-plugin");
|
||||||
|
|
||||||
var externals = {
|
var externals = {
|
||||||
"cockpit": "cockpit",
|
cockpit: "cockpit",
|
||||||
};
|
};
|
||||||
|
|
||||||
/* These can be overridden, typically from the Makefile.am */
|
/* These can be overridden, typically from the Makefile.am */
|
||||||
const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src";
|
const srcdir = (process.env.SRCDIR || __dirname) + path.sep + "src";
|
||||||
const builddir = (process.env.SRCDIR || __dirname);
|
const builddir = process.env.SRCDIR || __dirname;
|
||||||
const distdir = builddir + path.sep + "dist";
|
const distdir = builddir + path.sep + "dist";
|
||||||
const section = process.env.ONLYDIR || null;
|
const section = process.env.ONLYDIR || null;
|
||||||
const libdir = path.resolve(srcdir, "pkg" + path.sep + "lib");
|
const libdir = path.resolve(srcdir, "pkg" + path.sep + "lib")
|
||||||
const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules");
|
const nodedir = path.resolve(process.env.SRCDIR || __dirname, "node_modules");
|
||||||
|
|
||||||
/* A standard nodejs and webpack pattern */
|
/* A standard nodejs and webpack pattern */
|
||||||
var production = process.env.NODE_ENV === 'production';
|
var production = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
var info = {
|
var info = {
|
||||||
entries: {
|
entries: {
|
||||||
"recordings": [
|
index: [
|
||||||
"./recordings.jsx",
|
"./index.js",
|
||||||
"./recordings.css",
|
|
||||||
"./pkg/lib/listing.less",
|
|
||||||
],
|
],
|
||||||
"config": [
|
|
||||||
"./config.jsx",
|
|
||||||
"./recordings.css",
|
|
||||||
"./table.css",
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
files: [
|
files: [
|
||||||
"index.html",
|
"index.html",
|
||||||
"config.html",
|
"manifest.json"
|
||||||
"player.jsx",
|
|
||||||
"player.css",
|
|
||||||
"recordings.jsx",
|
|
||||||
"recordings.css",
|
|
||||||
"table.css",
|
|
||||||
"manifest.json",
|
|
||||||
"timer.css",
|
|
||||||
"./pkg/lib/listing.less",
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -64,8 +49,7 @@ var output = {
|
||||||
function vpath(/* ... */) {
|
function vpath(/* ... */) {
|
||||||
var filename = Array.prototype.join.call(arguments, path.sep);
|
var filename = Array.prototype.join.call(arguments, path.sep);
|
||||||
var expanded = builddir + path.sep + filename;
|
var expanded = builddir + path.sep + filename;
|
||||||
if (fs.existsSync(expanded))
|
if (fs.existsSync(expanded)) return expanded;
|
||||||
return expanded;
|
|
||||||
expanded = srcdir + path.sep + filename;
|
expanded = srcdir + path.sep + filename;
|
||||||
return expanded;
|
return expanded;
|
||||||
}
|
}
|
||||||
|
|
@ -78,10 +62,8 @@ Object.keys(info.entries).forEach(function(key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
info.entries[key] = info.entries[key].map(function (value) {
|
info.entries[key] = info.entries[key].map(function (value) {
|
||||||
if (value.indexOf("/") === -1)
|
if (value.indexOf("/") === -1) return value;
|
||||||
return value;
|
else return vpath(value);
|
||||||
else
|
|
||||||
return vpath(value);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -93,79 +75,124 @@ info.files.forEach(function(value) {
|
||||||
});
|
});
|
||||||
info.files = files;
|
info.files = files;
|
||||||
|
|
||||||
var plugins = [
|
var plugins = [new copy(info.files), new extract({ filename: "[name].css" })];
|
||||||
new copy(info.files),
|
|
||||||
new extract("[name].css")
|
|
||||||
];
|
|
||||||
|
|
||||||
/* Only minimize when in production mode */
|
/* Only minimize when in production mode */
|
||||||
if (production) {
|
if (production) {
|
||||||
/* Rename output files when minimizing */
|
/* Rename output files when minimizing */
|
||||||
output.filename = "[name].min.js";
|
output.filename = "[name].min.js";
|
||||||
|
|
||||||
plugins.unshift(new CompressionPlugin({
|
plugins.unshift(
|
||||||
|
new CompressionPlugin({
|
||||||
asset: "[path].gz[query]",
|
asset: "[path].gz[query]",
|
||||||
test: /\.(js|html)$/,
|
test: /\.(js|html)$/,
|
||||||
minRatio: 0.9,
|
minRatio: 0.9,
|
||||||
deleteOriginalAssets: true
|
deleteOriginalAssets: true,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var babel_loader = {
|
||||||
|
loader: "babel-loader",
|
||||||
|
options: {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
"@babel/env",
|
||||||
|
{
|
||||||
|
targets: {
|
||||||
|
chrome: "57",
|
||||||
|
firefox: "52",
|
||||||
|
safari: "10.3",
|
||||||
|
edge: "16",
|
||||||
|
opera: "44",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@babel/preset-react",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: production ? 'production' : 'development',
|
mode: production ? "production" : "development",
|
||||||
|
resolve: {
|
||||||
|
modules: [libdir, nodedir],
|
||||||
|
},
|
||||||
entry: info.entries,
|
entry: info.entries,
|
||||||
externals: externals,
|
externals: externals,
|
||||||
output: output,
|
output: output,
|
||||||
devtool: "source-map",
|
devtool: "source-map",
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"fs": path.resolve(nodedir, "fs-extra"),
|
|
||||||
},
|
|
||||||
modules: [libdir, nodedir],
|
|
||||||
},
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
enforce: 'pre',
|
enforce: "pre",
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: 'eslint-loader',
|
loader: "eslint-loader",
|
||||||
test: /\.jsx$/
|
test: /\.(js|jsx)$/,
|
||||||
},
|
|
||||||
{
|
|
||||||
enforce: 'pre',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
loader: 'eslint-loader',
|
|
||||||
test: /\.es6$/
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: 'babel-loader',
|
use: babel_loader,
|
||||||
test: /\.js$/
|
test: /\.(js|jsx)$/,
|
||||||
|
},
|
||||||
|
/* HACK: remove unwanted fonts from PatternFly's css */
|
||||||
|
{
|
||||||
|
test: /patternfly-4-cockpit.scss$/,
|
||||||
|
use: [
|
||||||
|
extract.loader,
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
sourceMap: true,
|
||||||
|
url: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exclude: /node_modules/,
|
loader: "string-replace-loader",
|
||||||
loader: 'babel-loader',
|
options: {
|
||||||
test: /\.jsx$/
|
multiple: [
|
||||||
|
{
|
||||||
|
search: /src:url\("patternfly-icons-fake-path\/pficon[^}]*/g,
|
||||||
|
replace: "src:url('fonts/patternfly.woff')format('woff');",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exclude: /node_modules/,
|
search: /@font-face[^}]*patternfly-fonts-fake-path[^}]*}/g,
|
||||||
loader: 'babel-loader',
|
replace: "",
|
||||||
test: /\.es6$/
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.less$/,
|
loader: "sass-loader",
|
||||||
loader: extract.extract("css-loader!less-loader")
|
options: {
|
||||||
|
sourceMap: true,
|
||||||
|
outputStyle: "compressed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
exclude: /node_modules/,
|
test: /\.s?css$/,
|
||||||
loader: extract.extract('css-loader!sass-loader'),
|
exclude: /patternfly-4-cockpit.scss/,
|
||||||
test: /\.scss$/
|
use: [
|
||||||
|
extract.loader,
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
sourceMap: true,
|
||||||
|
url: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: extract.extract("css-loader?minimize=&root=" + libdir),
|
loader: "sass-loader",
|
||||||
test: /\.css$/,
|
options: {
|
||||||
}
|
sourceMap: true,
|
||||||
]
|
outputStyle: "compressed",
|
||||||
},
|
},
|
||||||
plugins: plugins
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: plugins,
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue