@@ -100,7 +131,7 @@ function ErrorItem(props) {
);
}
-let ErrorService = class {
+const ErrorService = class {
constructor() {
this.addMessage = this.addMessage.bind(this);
this.errors = [];
@@ -125,7 +156,7 @@ let ErrorService = class {
/*
* An auto-loading buffer of recording's packets.
*/
-let PacketBuffer = class {
+const PacketBuffer = class {
/*
* Initialize a buffer.
*/
@@ -181,7 +212,7 @@ let PacketBuffer = class {
/* The journalctl reading the recording */
this.journalctl = Journal.journalctl(
this.matchList,
- {count: "all", follow: false, merge: true});
+ { count: "all", follow: false, merge: true });
this.journalctl.fail(this.handleError);
this.journalctl.stream(this.handleStream);
this.journalctl.done(this.handleDone);
@@ -300,7 +331,7 @@ let PacketBuffer = class {
this.pktList.push(pkt);
/* Notify any matching listeners */
while (this.idxDfdList.length > 0) {
- let idxDfd = this.idxDfdList[0];
+ const idxDfd = this.idxDfdList[0];
if (idxDfd[0] < this.pktList.length) {
this.idxDfdList.shift();
idxDfd[1].resolve();
@@ -363,10 +394,12 @@ let PacketBuffer = class {
break;
}
if (io.length > 0) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
+ this.addPacket({
+ pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()
+ });
io = [];
}
this.pos += x;
@@ -379,10 +412,12 @@ let PacketBuffer = class {
break;
}
if (io.length > 0 && is_output) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
+ this.addPacket({
+ pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()
+ });
io = [];
}
is_output = false;
@@ -401,10 +436,12 @@ let PacketBuffer = class {
break;
}
if (io.length > 0 && !is_output) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
+ this.addPacket({
+ pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()
+ });
io = [];
}
is_output = true;
@@ -423,16 +460,20 @@ let PacketBuffer = class {
break;
}
if (io.length > 0) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
+ this.addPacket({
+ pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()
+ });
io = [];
}
- this.addPacket({pos: this.pos,
- is_io: false,
- width: x,
- height: y});
+ this.addPacket({
+ pos: this.pos,
+ is_io: false,
+ width: x,
+ height: y
+ });
this.width = x;
this.height = y;
break;
@@ -447,10 +488,12 @@ let PacketBuffer = class {
}
if (io.length > 0) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
+ this.addPacket({
+ pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()
+ });
}
}
@@ -517,7 +560,7 @@ let PacketBuffer = class {
if (!('__CURSOR' in e)) {
this.handleError("No cursor in a Journal entry");
}
- this.cursor = e['__CURSOR'];
+ this.cursor = e.__CURSOR;
}
/* TODO Refer to entry number/cursor in errors */
if (!('MESSAGE' in e)) {
@@ -525,16 +568,16 @@ let PacketBuffer = class {
}
/* Parse the entry message */
try {
- let utf8decoder = new TextDecoder();
+ const utf8decoder = new TextDecoder();
/* Journalctl stores fields with non-printable characters
* in an array of raw bytes formatted as unsigned
* integers */
- if (Array.isArray(e['MESSAGE'])) {
- let u8arr = new Uint8Array(e['MESSAGE']);
+ if (Array.isArray(e.MESSAGE)) {
+ const u8arr = new Uint8Array(e.MESSAGE);
this.parseMessage(JSON.parse(utf8decoder.decode(u8arr)));
} else {
- this.parseMessage(JSON.parse(e['MESSAGE']));
+ this.parseMessage(JSON.parse(e.MESSAGE));
}
} catch (error) {
this.handleError(error);
@@ -555,8 +598,10 @@ let PacketBuffer = class {
/* Continue with the "following" run */
this.journalctl = Journal.journalctl(
this.matchList,
- {cursor: this.cursor,
- follow: true, merge: true, count: "all"});
+ {
+ cursor: this.cursor,
+ follow: true, merge: true, count: "all"
+ });
this.journalctl.fail(this.handleError);
this.journalctl.stream(this.handleStream);
/* NOTE: no "done" handler on purpose */
@@ -582,20 +627,18 @@ class Search extends React.Component {
};
}
- handleInputChange(event) {
+ handleInputChange(name, value) {
event.preventDefault();
- const name = event.target.name;
- const value = event.target.value;
- let state = {};
+ const state = {};
state[name] = value;
this.setState(state);
- cockpit.location.go(cockpit.location.path[0], $.extend(cockpit.location.options, {search_rec: value}));
+ cockpit.location.go(cockpit.location.path[0], $.extend(cockpit.location.options, { search_rec: value }));
}
handleSearchSubmit() {
this.journalctl = Journal.journalctl(
this.props.matchList,
- {count: "all", follow: false, merge: true, grep: this.state.search});
+ { count: "all", follow: false, merge: true, grep: this.state.search });
this.journalctl.fail(this.handleError);
this.journalctl.stream(this.handleStream);
}
@@ -605,9 +648,15 @@ class Search extends React.Component {
return JSON.parse(item.MESSAGE);
});
items = items.map(item => {
- return
;
+ return (
+
+ );
});
- this.setState({items: items});
+ this.setState({ items: items });
}
handleError(data) {
@@ -617,7 +666,7 @@ class Search extends React.Component {
clearSearchResults() {
delete cockpit.location.options.search;
cockpit.location.go(cockpit.location.path[0], cockpit.location.options);
- this.setState({search: ""});
+ this.setState({ search: "" });
this.handleStream([]);
}
@@ -629,18 +678,28 @@ class Search extends React.Component {
render() {
return (
-
-
-
-
-
-
-
-
-
+
+
+ this.handleInputChange("search", value)} />
+
+
+
+
{this.state.items}
-
-
+
+
);
}
}
@@ -655,12 +714,6 @@ class InputPlayer extends React.Component {
}
}
-function Slider(props) {
- return (
-
- );
-}
-
export class Player extends React.Component {
constructor(props) {
super(props);
@@ -677,9 +730,6 @@ export class Player extends React.Component {
this.speedReset = this.speedReset.bind(this);
this.fastForwardToEnd = this.fastForwardToEnd.bind(this);
this.skipFrame = this.skipFrame.bind(this);
- this.initSlider = this.initSlider.bind(this);
- this.slideStart = this.slideStart.bind(this);
- this.slideStop = this.slideStop.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.sync = this.sync.bind(this);
this.zoomIn = this.zoomIn.bind(this);
@@ -692,6 +742,8 @@ export class Player extends React.Component {
this.fastForwardToTS = this.fastForwardToTS.bind(this);
this.sendInput = this.sendInput.bind(this);
this.clearInputPlayer = this.clearInputPlayer.bind(this);
+ this.handleInfoClick = this.handleInfoClick.bind(this);
+ this.handleProgressClick = this.handleProgressClick.bind(this);
this.state = {
cols: 80,
@@ -722,6 +774,8 @@ export class Player extends React.Component {
scale: 1,
input: "",
mark: 0,
+ infoEnabled: false,
+ curTS: 0,
};
this.containerHeight = 400;
@@ -731,9 +785,6 @@ export class Player extends React.Component {
this.reportError = this.error_service.addMessage;
this.buf = new PacketBuffer(this.props.matchList, this.reportError);
- /* Slider component */
- this.slider = null;
-
/* Current recording time, ms */
this.recTS = 0;
/* Corresponding local time, ms */
@@ -746,8 +797,6 @@ export class Player extends React.Component {
/* Timeout ID of the current packet, null if none */
this.timeout = null;
- this.currentTsPost = 0;
-
/* True if the next packet should be output without delay */
this.skip = false;
/* Playback speed */
@@ -757,9 +806,6 @@ export class Player extends React.Component {
* Recording time, ms, or null if not fast-forwarding.
*/
this.fastForwardTo = null;
-
- /* Track paused state prior to slider movement */
- this.pausedBeforeSlide = true;
}
reset() {
@@ -781,7 +827,6 @@ export class Player extends React.Component {
/* Move to beginning of recording */
this.recTS = 0;
- this.currentTsPost = parseInt(this.recTS);
/* Start the playback time */
this.locTS = performance.now();
@@ -851,7 +896,7 @@ export class Player extends React.Component {
sendInput(pkt) {
if (pkt) {
const current_input = this.state.input;
- this.setState({input: current_input + pkt.io});
+ this.setState({ input: current_input + pkt.io });
}
}
@@ -866,7 +911,7 @@ export class Player extends React.Component {
for (;;) {
/* Get another packet to output, if none */
for (; this.pkt === null; this.pktIdx++) {
- let pkt = this.buf.pktList[this.pktIdx];
+ const pkt = this.buf.pktList[this.pktIdx];
/* If there are no more packets */
if (pkt === undefined) {
/*
@@ -886,7 +931,7 @@ export class Player extends React.Component {
}
/* Get the current local time */
- let nowLocTS = performance.now();
+ const nowLocTS = performance.now();
/* Ignore the passed time, if we're paused */
if (this.state.paused) {
@@ -898,7 +943,6 @@ export class Player extends React.Component {
/* Sync to the local time */
this.locTS = nowLocTS;
- this.slider.slider('setAttribute', 'max', this.buf.pos);
/* If we are skipping one packet's delay */
if (this.skip) {
this.skip = false;
@@ -918,10 +962,8 @@ export class Player extends React.Component {
return;
} else {
this.recTS += locDelay * this.speed;
- let pktRecDelay = this.pkt.pos - this.recTS;
- let pktLocDelay = pktRecDelay / this.speed;
- this.currentTsPost = parseInt(this.recTS);
- this.slider.slider('setValue', this.currentTsPost);
+ const pktRecDelay = this.pkt.pos - this.recTS;
+ const pktLocDelay = pktRecDelay / this.speed;
/* If we're more than 5 ms early for this packet */
if (pktLocDelay > 5) {
/* Call us again on time, later */
@@ -934,8 +976,7 @@ export class Player extends React.Component {
if (this.props.logsEnabled) {
this.props.onTsChange(this.pkt.pos);
}
- this.currentTsPost = parseInt(this.pkt.pos);
- this.slider.slider('setValue', this.currentTsPost);
+ this.setState({ curTS: this.pkt.pos });
/* Output the packet */
if (this.pkt.is_io && !this.pkt.is_output) {
@@ -955,37 +996,37 @@ export class Player extends React.Component {
}
playPauseToggle() {
- this.setState({paused: !this.state.paused});
+ this.setState({ paused: !this.state.paused });
}
play() {
- this.setState({paused: false});
+ this.setState({ paused: false });
}
pause() {
- this.setState({paused: true});
+ this.setState({ paused: true });
}
speedUp() {
- let speedExp = this.state.speedExp;
+ const speedExp = this.state.speedExp;
if (speedExp < 4) {
- this.setState({speedExp: speedExp + 1});
+ this.setState({ speedExp: speedExp + 1 });
}
}
speedDown() {
- let speedExp = this.state.speedExp;
+ const speedExp = this.state.speedExp;
if (speedExp > -4) {
- this.setState({speedExp: speedExp - 1});
+ this.setState({ speedExp: speedExp - 1 });
}
}
speedReset() {
- this.setState({speedExp: 0});
+ this.setState({ speedExp: 0 });
}
clearInputPlayer() {
- this.setState({input: ""});
+ this.setState({ input: "" });
}
rewindToStart() {
@@ -1015,49 +1056,19 @@ export class Player extends React.Component {
this.sync();
}
- initSlider() {
- this.slider = $("#slider").slider({
- value: 0,
- tooltip: "hide",
- enabled: false,
- });
- this.slider.slider('on', 'slideStart', this.slideStart);
- this.slider.slider('on', 'slideStop', this.slideStop);
- this.slider.slider('enable');
- }
-
- slideStart(e) {
- /*
- * Necessary because moving the slider position updates state.paused,
- * which won't represent the actual paused state after this event is
- * triggered
- */
- this.pausedBeforeSlide = this.state.paused;
- this.pause();
- }
-
- slideStop(e) {
- if (this.fastForwardToTS) {
- this.fastForwardToTS(e);
- if (this.pausedBeforeSlide === false) {
- this.play();
- }
- }
- }
-
handleKeyDown(event) {
- let keyCodesFuncs = {
- "P": this.playPauseToggle,
+ const keyCodesFuncs = {
+ P: this.playPauseToggle,
"}": this.speedUp,
"{": this.speedDown,
- "Backspace": this.speedReset,
+ Backspace: this.speedReset,
".": this.skipFrame,
- "G": this.fastForwardToEnd,
- "R": this.rewindToStart,
+ G: this.fastForwardToEnd,
+ R: this.rewindToStart,
"+": this.zoomIn,
"=": this.zoomIn,
"-": this.zoomOut,
- "Z": this.fitIn,
+ Z: this.fitIn,
};
if (event.target.nodeName.toLowerCase() !== 'input') {
if (keyCodesFuncs[event.key]) {
@@ -1088,30 +1099,30 @@ export class Player extends React.Component {
}
dragPanEnable() {
- this.setState({drag_pan: true});
+ this.setState({ drag_pan: true });
- let scrollwrap = this.refs.scrollwrap;
+ const scrollwrap = this.refs.scrollwrap;
let clicked = false;
let clickX;
let clickY;
$(this.refs.scrollwrap).on({
- 'mousemove': function(e) {
+ mousemove: function(e) {
clicked && updateScrollPos(e);
},
- 'mousedown': function(e) {
+ mousedown: function(e) {
clicked = true;
clickY = e.pageY;
clickX = e.pageX;
},
- 'mouseup': function() {
+ mouseup: function() {
clicked = false;
$('html').css('cursor', 'auto');
}
});
- let updateScrollPos = function(e) {
+ const updateScrollPos = function(e) {
$('html').css('cursor', 'move');
$(scrollwrap).scrollTop($(scrollwrap).scrollTop() + (clickY - e.pageY));
$(scrollwrap).scrollLeft($(scrollwrap).scrollLeft() + (clickX - e.pageX));
@@ -1119,8 +1130,8 @@ export class Player extends React.Component {
}
dragPanDisable() {
- this.setState({drag_pan: false});
- let scrollwrap = this.refs.scrollwrap;
+ this.setState({ drag_pan: false });
+ const scrollwrap = this.refs.scrollwrap;
$(scrollwrap).off("mousemove");
$(scrollwrap).off("mousedown");
$(scrollwrap).off("mouseup");
@@ -1132,7 +1143,7 @@ export class Player extends React.Component {
scale = scale + 0.1;
this.zoom(scale);
} else {
- this.setState({term_zoom_max: true});
+ this.setState({ term_zoom_max: true });
}
}
@@ -1142,7 +1153,7 @@ export class Player extends React.Component {
scale = scale - 0.1;
this.zoom(scale);
} else {
- this.setState({term_zoom_min: true});
+ this.setState({ term_zoom_min: true });
}
}
@@ -1163,7 +1174,7 @@ export class Player extends React.Component {
window.addEventListener("keydown", this.handleKeyDown, false);
if (this.refs.wrapper.offsetWidth) {
- this.setState({containerWidth: this.refs.wrapper.offsetWidth});
+ this.setState({ containerWidth: this.refs.wrapper.offsetWidth });
}
/* Open the terminal */
this.state.term.open(this.refs.term);
@@ -1171,7 +1182,6 @@ export class Player extends React.Component {
/* Reset playback */
this.reset();
this.fastForwardToTS(0);
- this.initSlider();
}
componentDidUpdate(prevProps, prevState) {
@@ -1189,11 +1199,21 @@ export class Player extends React.Component {
}
}
- render() {
- let r = this.props.recording;
+ handleInfoClick() {
+ this.setState({ infoEnabled: !this.state.infoEnabled });
+ }
- let speedExp = this.state.speedExp;
- let speedFactor = Math.pow(2, Math.abs(speedExp));
+ handleProgressClick(e) {
+ const progress = Math.min(1, Math.max(0, e.clientX / $(".pf-c-progress__bar").width()));
+ const ts = Math.round(progress * this.buf.pos);
+ this.fastForwardToTS(ts);
+ }
+
+ render() {
+ const r = this.props.recording;
+
+ const speedExp = this.state.speedExp;
+ const speedFactor = Math.pow(2, Math.abs(speedExp));
let speedStr;
if (speedExp > 0) {
@@ -1205,156 +1225,246 @@ export class Player extends React.Component {
}
const style = {
- "transform": "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")",
- "transformOrigin": "top left",
- "display": "inline-block",
- "margin": "0 auto",
- "position": "absolute",
- "top": this.state.term_top_style,
- "left": this.state.term_left_style,
+ transform: "scale(" + this.state.scale + ") translate(" + this.state.term_translate + ")",
+ transformOrigin: "top left",
+ display: "inline-block",
+ margin: "0 auto",
+ position: "absolute",
+ top: this.state.term_top_style,
+ left: this.state.term_left_style,
};
const scrollwrap = {
- "minWidth": "630px",
- "height": this.containerHeight + "px",
- "backgroundColor": "#f5f5f5",
- "overflow": this.state.term_scroll,
- "position": "relative",
+ minWidth: "630px",
+ height: this.containerHeight + "px",
+ backgroundColor: "#f5f5f5",
+ overflow: this.state.term_scroll,
+ position: "relative",
};
- const to_right = {
- "float": "right",
- };
+ const timeStr = formatDuration(this.state.curTS) +
+ " / " +
+ formatDuration(this.buf.pos);
+
+ const progress = (
+
+ );
+
+ const playbackControls = (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {speedStr !== "" &&
+
+
+
+ {speedStr}
+
+
+ }
+
+ );
+
+ const visualControls = (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ const panel = (
+
+
+ {playbackControls}
+ {visualControls}
+
+
+
+
+
+ );
+
+ const recordingInfo = (
+
+ );
+
+ const infoSection = (
+
+ {recordingInfo}
+
+ );
// ensure react never reuses this div by keying it with the terminal widget
return (
-
-
-
-
-
-
- {this.state.title}
-
-
-
-
-
-
-
-
-
-
-
-
{speedStr}
-
- {formatDuration(this.currentTsPost)} / {formatDuration(this.buf.pos)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {_("Recording")}
-
-
-
-
-
- | {_("ID")} |
- {r.id} |
-
-
- | {_("Hostname")} |
- {r.hostname} |
-
-
- | {_("Boot ID")} |
- {r.boot_id} |
-
-
- | {_("Session ID")} |
- {r.session_id} |
-
-
- | {_("PID")} |
- {r.pid} |
-
-
- | {_("Start")} |
- {formatDateTime(r.start)} |
-
-
- | {_("End")} |
- {formatDateTime(r.end)} |
-
-
- | {_("Duration")} |
- {formatDuration(r.end - r.start)} |
-
-
- | {_("User")} |
- {r.user} |
-
-
-
-
+ <>
+
+
+ {this.state.title}
+
+
+ {progress}
+ {panel}
-
+ {infoSection}
+ >
);
}
diff --git a/src/plot.css b/src/plot.css
deleted file mode 100644
index 167a863..0000000
--- a/src/plot.css
+++ /dev/null
@@ -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;
-}
diff --git a/src/recordings.css b/src/recordings.css
deleted file mode 100644
index d227c1c..0000000
--- a/src/recordings.css
+++ /dev/null
@@ -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;
-}
diff --git a/src/recordings.jsx b/src/recordings.jsx
index 9407960..c3bf729 100644
--- a/src/recordings.jsx
+++ b/src/recordings.jsx
@@ -19,26 +19,61 @@
"use strict";
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");
-let cockpit = require("cockpit");
-let _ = cockpit.gettext;
-let moment = require("moment");
-let Journal = require("journal");
-let Listing = require("cockpit-components-listing.jsx");
-let Player = require("./player.jsx");
-
-require("bootstrap-datetime-picker/js/bootstrap-datetimepicker.js");
-require("bootstrap-datetime-picker/css/bootstrap-datetimepicker.css");
+const $ = require("jquery");
+const cockpit = require("cockpit");
+const _ = cockpit.gettext;
+const moment = require("moment");
+const Journal = require("journal");
+const Player = require("./player.jsx");
+const Config = require("./config.jsx");
/*
* Convert a number to integer number string and pad with zeroes to
* specified width.
*/
-let padInt = function (n, w) {
- let i = Math.floor(n);
- let a = Math.abs(i);
+const padInt = function (n, w) {
+ const i = Math.floor(n);
+ const a = Math.abs(i);
let s = a.toString();
for (w -= s.length; w > 0; w--) {
s = '0' + s;
@@ -49,16 +84,16 @@ let padInt = function (n, w) {
/*
* 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");
};
-let formatDateTimeOffset = function (ms, offset) {
+const formatDateTimeOffset = function (ms, offset) {
return moment(ms).utcOffset(offset)
.format("YYYY-MM-DD HH:mm:ss");
};
-let formatUTC = function(date) {
+const formatUTC = function(date) {
return moment(date).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.
*/
-let formatDuration = function (ms) {
+const formatDuration = function (ms) {
let v = Math.floor(ms / 1000);
- let s = Math.floor(v % 60);
+ const s = Math.floor(v % 60);
v = Math.floor(v / 60);
- let m = Math.floor(v % 60);
+ const m = Math.floor(v % 60);
v = Math.floor(v / 60);
- let h = Math.floor(v % 24);
- let d = Math.floor(v / 24);
+ const h = Math.floor(v % 24);
+ const d = Math.floor(v / 24);
let str = '';
if (d > 0) {
@@ -89,100 +124,13 @@ let formatDuration = function (ms) {
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 (
-
-
-
-
-
-
- );
- }
-}
-
function LogElement(props) {
const entry = props.entry;
const start = props.start;
- const end = props.end;
const cursor = entry.__CURSOR;
const entry_timestamp = parseInt(entry.__REALTIME_TIMESTAMP / 1000);
- const timeClick = function(e) {
+ const timeClick = function(_e) {
const ts = entry_timestamp - start;
if (ts > 0) {
props.jumpToTs(ts);
@@ -196,33 +144,38 @@ function LogElement(props) {
win.focus();
};
- let className = 'cockpit-logline';
- if (start < entry_timestamp && end > entry_timestamp) {
- className = 'cockpit-logline highlighted';
- }
+ const cells =
+
+
+
+ {entry.MESSAGE}
+
+
+ ]} />;
return (
-
-
-
-
-
{formatDateTime(entry_timestamp)}
-
{entry.MESSAGE}
-
+
+ {cells}
+
);
}
function LogsView(props) {
- const entries = props.entries;
- const start = props.start;
- const end = props.end;
+ const { entries, start, end } = props;
const rows = entries.map((entry) =>
-
+
);
return (
-
- {rows}
-
+
);
}
@@ -252,23 +205,13 @@ class Logs extends React.Component {
getServerTimeOffset() {
cockpit.spawn(["date", "+%s:%:z"], { err: "message" })
.done((data) => {
- this.setState({serverTimeOffset: data.slice(data.indexOf(":") + 1)});
+ this.setState({ serverTimeOffset: data.slice(data.indexOf(":") + 1) });
})
.fail((ex) => {
console.log("Couldn't calculate server time offset: " + cockpit.message(ex));
});
}
- 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) {
console.warn(cockpit.message(error));
}
@@ -277,14 +220,13 @@ class Logs extends React.Component {
if (entryList.length > 0) {
this.entries.push(...entryList);
const after = this.entries[this.entries.length - 1].__CURSOR;
- this.setState({entries: this.entries, after: after});
- this.scrollToBottom();
+ this.setState({ entries: this.entries, after: after });
}
}
journalctlPrepend(entryList) {
entryList.push(...this.entries);
- this.setState({entries: this.entries});
+ this.setState({ entries: this.entries });
}
getLogs() {
@@ -294,7 +236,7 @@ class Logs extends React.Component {
this.journalCtl = null;
}
- let matches = [];
+ const matches = [];
if (this.hostname) {
matches.push("_HOSTNAME=" + this.hostname);
}
@@ -310,7 +252,7 @@ class Logs extends React.Component {
end = formatDateTime(this.end);
}
- let options = {
+ const options = {
since: start,
until: end,
follow: false,
@@ -319,7 +261,7 @@ class Logs extends React.Component {
};
if (this.state.after != null) {
- options["after"] = this.state.after;
+ options.after = this.state.after;
delete options.since;
}
@@ -375,20 +317,36 @@ class Logs extends React.Component {
}
render() {
- let r = this.props.recording;
+ const r = this.props.recording;
if (r == null) {
- return Loading...;
+ return (
+
+
+
+
+ {_("Loading...")}
+
+
+
+ );
} else {
return (
-
-
- {_("Logs")}
-
-
-
-
-
+ <>
+
+
+ }
+ onClick={this.loadLater}>
+ {_("Load later entries")}
+
+
+ >
);
}
}
@@ -416,20 +374,20 @@ class Recording extends React.Component {
}
handleTsChange(ts) {
- this.setState({curTs: ts});
+ this.setState({ curTs: ts });
}
handleLogTsChange(ts) {
- this.setState({logsTs: ts});
+ this.setState({ logsTs: ts });
}
handleLogsClick() {
- this.setState({logsEnabled: !this.state.logsEnabled});
+ this.setState({ logsEnabled: !this.state.logsEnabled });
}
handleLogsReset() {
- this.setState({logsEnabled: false}, () => {
- this.setState({logsEnabled: true});
+ this.setState({ logsEnabled: false }, () => {
+ this.setState({ logsEnabled: true });
});
}
@@ -445,47 +403,44 @@ class Recording extends React.Component {
}
render() {
- let r = this.props.recording;
+ const r = this.props.recording;
if (r == null) {
- return Loading...;
- } else {
- let player =
- ();
-
return (
-
-
-
-
- {this.state.logsEnabled === true &&
-
- }
-
-
+
+
+
+
+ {_("Loading...")}
+
+
+
+ );
+ } else {
+ return (
+ <>
+ } onClick={this.goBackToList}>
+ {_("Session Recording")}
+
+
+
+
+
+ >
);
}
}
@@ -499,119 +454,82 @@ class Recording extends React.Component {
class RecordingList extends React.Component {
constructor(props) {
super(props);
- this.handleColumnClick = this.handleColumnClick.bind(this);
- this.getSortedList = this.getSortedList.bind(this);
- this.drawSortDir = this.drawSortDir.bind(this);
- this.getColumnTitles = this.getColumnTitles.bind(this);
- this.getColumns = this.getColumns.bind(this);
+
+ this.onSort = this.onSort.bind(this);
+ this.rowClickHandler = this.rowClickHandler.bind(this);
this.state = {
- sorting_field: "start",
- sorting_asc: true,
+ sortBy: {
+ index: 1,
+ direction: SortByDirection.asc
+ }
};
}
- drawSortDir() {
- $('#sort_arrow').remove();
- let type = this.state.sorting_asc ? "asc" : "desc";
- let arrow = '';
- $(this.refs[this.state.sorting_field]).append(arrow);
+ onSort(_event, index, direction) {
+ this.setState({
+ sortBy: {
+ index,
+ direction
+ },
+ });
}
- handleColumnClick(event) {
- if (this.state.sorting_field === event.currentTarget.id) {
- this.setState({sorting_asc: !this.state.sorting_asc});
- } else {
- this.setState({
- sorting_field: event.currentTarget.id,
- sorting_asc: true,
- });
- }
- }
-
- getSortedList() {
- let field = this.state.sorting_field;
- 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 = [
- (),
- (),
- (),
- (),
- ];
- if (this.props.diff_hosts === true) {
- columnTitles.push(());
- }
- 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;
+ rowClickHandler(_event, row) {
+ cockpit.location.go([row.id], cockpit.location.options);
}
render() {
- let columnTitles = this.getColumnTitles();
- let list = this.getSortedList();
- let rows = [];
+ const { sortBy } = this.state;
+ const { index, direction } = sortBy;
+
+ // 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();
- }
return (
-
- {rows}
-
+ <>
+
+ {!rows.length &&
+
+
+
+ {_("No recordings found")}
+
+
+ {_("No recordings matched the filter criteria.")}
+
+ }
+ >
);
}
}
@@ -621,13 +539,12 @@ class RecordingList extends React.Component {
* single recording. Extracts the ID of the recording to display from
* 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) {
super(props);
this.onLocationChanged = this.onLocationChanged.bind(this);
this.journalctlIngest = this.journalctlIngest.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
- this.handleDateSinceChange = this.handleDateSinceChange.bind(this);
this.openConfig = this.openConfig.bind(this);
/* Journalctl instance */
this.journalctl = null;
@@ -637,11 +554,12 @@ class View extends React.Component {
this.recordingMap = {};
/* tlog UID in system set in ComponentDidMount */
this.uid = null;
+ const path = cockpit.location.path[0];
this.state = {
/* List of recordings in start order */
recordingList: [],
/* 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 */
date_since: cockpit.location.options.date_since || "",
date_until: cockpit.location.options.date_until || "",
@@ -651,6 +569,8 @@ class View extends React.Component {
/* filter values end */
error_tlog_uid: false,
diff_hosts: false,
+ /* if config is open */
+ config: path === "config",
};
}
@@ -666,63 +586,70 @@ class View extends React.Component {
* displayed recording ID.
*/
onLocationChanged() {
- this.setState({
- recordingID: cockpit.location.path[0] || null,
- date_since: cockpit.location.options.date_since || "",
- date_until: cockpit.location.options.date_until || "",
- username: cockpit.location.options.username || "",
- hostname: cockpit.location.options.hostname || "",
- search: cockpit.location.options.search || "",
- });
+ const path = cockpit.location.path[0];
+ if (path === "config")
+ this.setState({ config: true });
+ else
+ this.setState({
+ recordingID: cockpit.location.path[0] || null,
+ date_since: cockpit.location.options.date_since || "",
+ date_until: cockpit.location.options.date_until || "",
+ username: cockpit.location.options.username || "",
+ hostname: cockpit.location.options.hostname || "",
+ search: cockpit.location.options.search || "",
+ config: false
+ });
}
/*
* Ingest journal entries sent by journalctl.
*/
journalctlIngest(entryList) {
- let recordingList = this.state.recordingList.slice();
+ const recordingList = this.state.recordingList.slice();
let i;
let j;
let hostname;
if (entryList[0]) {
- if (entryList[0]["_HOSTNAME"]) {
- hostname = entryList[0]["_HOSTNAME"];
+ if (entryList[0]._HOSTNAME) {
+ hostname = entryList[0]._HOSTNAME;
}
}
for (i = 0; i < entryList.length; i++) {
- let e = entryList[i];
- let id = e['TLOG_REC'];
+ const e = entryList[i];
+ const id = e.TLOG_REC;
/* Skip entries with missing recording ID */
if (id === undefined) {
continue;
}
- let ts = Math.floor(
- parseInt(e["__REALTIME_TIMESTAMP"], 10) /
+ const ts = Math.floor(
+ parseInt(e.__REALTIME_TIMESTAMP, 10) /
1000);
let r = this.recordingMap[id];
/* If no recording found */
if (r === undefined) {
/* Create new recording */
- if (hostname !== e["_HOSTNAME"]) {
- this.setState({diff_hosts: true});
+ if (hostname !== e._HOSTNAME) {
+ this.setState({ diff_hosts: true });
}
- r = {id: id,
- matchList: ["TLOG_REC=" + id],
- user: e["TLOG_USER"],
- boot_id: e["_BOOT_ID"],
- session_id: parseInt(e["TLOG_SESSION"], 10),
- pid: parseInt(e["_PID"], 10),
- start: ts,
- /* FIXME Should be start + message duration */
- end: ts,
- hostname: e["_HOSTNAME"],
- duration: 0};
+ r = {
+ id: id,
+ matchList: ["TLOG_REC=" + id],
+ user: e.TLOG_USER,
+ boot_id: e._BOOT_ID,
+ session_id: parseInt(e.TLOG_SESSION, 10),
+ pid: parseInt(e._PID, 10),
+ start: ts,
+ /* FIXME Should be start + message duration */
+ end: ts,
+ hostname: e._HOSTNAME,
+ duration: 0
+ };
/* Map the recording */
this.recordingMap[id] = r;
/* Insert the recording in order */
@@ -757,7 +684,7 @@ class View extends React.Component {
}
}
- this.setState({recordingList: recordingList});
+ this.setState({ recordingList: recordingList });
}
/*
@@ -765,7 +692,7 @@ class View extends React.Component {
* Assumes journalctl is not running.
*/
journalctlStart() {
- let matches = ["_COMM=tlog-rec",
+ const matches = ["_COMM=tlog-rec",
/* Strings longer than TASK_COMM_LEN (16) characters
* are truncated (man proc) */
"_COMM=tlog-rec-sessio"];
@@ -777,22 +704,22 @@ class View extends React.Component {
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 !== "") {
- options['since'] = formatUTC(this.state.date_since);
+ options.since = formatUTC(this.state.date_since);
}
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) {
- options["grep"] = this.state.search;
+ options.grep = this.state.search;
}
if (this.state.recordingID !== null) {
- delete options["grep"];
+ delete options.grep;
matches.push("TLOG_REC=" + this.state.recordingID);
}
@@ -835,32 +762,22 @@ class View extends React.Component {
*/
clearRecordings() {
this.recordingMap = {};
- this.setState({recordingList: []});
+ this.setState({ recordingList: [] });
}
- handleInputChange(event) {
- const name = event.target.name;
- const value = event.target.value;
- let state = {};
+ handleInputChange(name, value) {
+ const state = {};
state[name] = value;
this.setState(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() {
- cockpit.jump(['session-recording/config']);
+ cockpit.location.go("/config");
}
componentDidMount() {
- let proc = cockpit.spawn(["getent", "passwd", "tlog"]);
+ const proc = cockpit.spawn(["getent", "passwd", "tlog"]);
proc.stream((data) => {
this.uid = data.split(":", 3)[2];
@@ -869,7 +786,7 @@ class View extends React.Component {
});
proc.fail(() => {
- this.setState({error_tlog_uid: true});
+ this.setState({ error_tlog_uid: true });
});
cockpit.addEventListener("locationchanged",
@@ -882,7 +799,7 @@ class View extends React.Component {
}
}
- componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate(_prevProps, prevState) {
/*
* If we're running a specific (non-wildcard) journalctl
* and recording ID has changed
@@ -906,75 +823,94 @@ class View extends React.Component {
}
render() {
- if (this.state.error_tlog_uid === true) {
+ if (this.state.config === true) {
+ return ;
+ } else if (this.state.error_tlog_uid === true) {
return (
-
- Error getting tlog UID from system.
-
+
+
+
+
+ {_("Error")}
+
+
+ {_("Unable to retrieve tlog UID from system.")}
+
+
+
);
- }
- if (this.state.recordingID === null) {
+ } else if (this.state.recordingID === null) {
+ const toolbar = (
+
+
+ {_("Since")}
+
+ this.handleInputChange("date_since", value)} />
+
+
+
+ {_("Until")}
+
+ this.handleInputChange("date_until", value)} />
+
+
+
+ {_("Search")}
+
+ this.handleInputChange("search", value)} />
+
+
+
+ {_("Username")}
+
+ this.handleInputChange("username", value)} />
+
+
+ {this.state.diff_hosts === true &&
+
+ {_("Hostname")}
+
+ this.handleInputChange("hostname", value)} />
+
+ }
+
+
+
+
+ );
+
return (
-
-
+ <>
+ {toolbar}
-
+ >
);
} else {
return (
-
-
-
+
);
}
}
}
-
-ReactDOM.render(, document.getElementById('view'));
diff --git a/src/table.css b/src/table.css
deleted file mode 100644
index b038fd0..0000000
--- a/src/table.css
+++ /dev/null
@@ -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%;
- }
-}
diff --git a/test/check-application b/test/check-application
index 02c5d6b..ca32071 100755
--- a/test/check-application
+++ b/test/check-application
@@ -18,12 +18,16 @@ class TestApplication(MachineCase):
self.login_and_go("/session-recording")
b = self.browser
m = self.machine
- b.wait_present(".content-header-extra")
- b.wait_present("#user")
+ b.wait_present("#app")
return b, m
- def _sel_rec(self, cond=":first()"):
- self.browser.click(f".listing-ct-item{cond}")
+ def _sel_rec(self, index=0):
+ page = (
+ "0f25700a28c44b599869745e5fda8b0c-7106-121e79"
+ if not index
+ else "0f25700a28c44b599869745e5fda8b0c-7623-135541"
+ )
+ self.browser.go(f"/session-recording#/{page}")
def _term_line(self, 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")
def testFastforwardControls(self):
- slider = ".slider > .min-slider-handle"
+ progress = ".pf-c-progress__indicator"
b, _ = self._login()
self._sel_rec()
# fast forward
b.click("#player-fast-forward")
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
b.click("#player-restart")
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):
b, _ = self._login()
@@ -54,7 +58,6 @@ class TestApplication(MachineCase):
# increase speed
b.wait_present("#player-speed-up")
b.click("#player-speed-up")
- b.wait_present("#player-speed")
b.wait_text("#player-speed", "x2")
b.click("#player-speed-up")
b.wait_text("#player-speed", "x4")
@@ -70,7 +73,6 @@ class TestApplication(MachineCase):
b.click("#player-speed-down")
b.wait_text("#player-speed", "x2")
b.click("#player-speed-down")
- b.wait_present("#player-speed")
b.click("#player-speed-down")
b.wait_text("#player-speed", "/2")
b.click("#player-speed-down")
@@ -80,8 +82,7 @@ class TestApplication(MachineCase):
b.click("#player-speed-down")
b.wait_text("#player-speed", "/16")
# restore speed
- b.click("#player-speed-reset")
- b.wait_present("#player-speed")
+ b.click(".pf-c-chip .pf-c-button")
b.click("#player-speed-down")
b.wait_text("#player-speed", "/2")
@@ -142,9 +143,7 @@ class TestApplication(MachineCase):
import time, json, configparser
b, m = self._login()
- # Ensure that the button leads to the config page
b.click("#btn-config")
- b.enter_page("/session-recording/config")
# TLOG config
conf_file_path = "/etc/tlog/"
@@ -179,19 +178,22 @@ class TestApplication(MachineCase):
m.upload([save_file], conf_file_path)
# Check that the config reflects the changes
conf = json.load(open(test_file, "r"))
- assert json.dumps(conf) == json.dumps(
- {
- "shell": "/test/path/shell",
- "notice": "Test Notice",
- "latency": 1,
- "payload": 2,
- "log": {"input": True, "output": False, "window": False},
- "limit": {"rate": 3, "burst": 4, "action": "drop"},
- "file": {"path": "/test/path/file"},
- "syslog": {"facility": "testfac", "priority": "info"},
- "journal": {"priority": "info", "augment": False},
- "writer": "file",
- }
+ self.assertEqual(
+ json.dumps(conf),
+ json.dumps(
+ {
+ "shell": "/test/path/shell",
+ "notice": "Test Notice",
+ "latency": 1,
+ "payload": 2,
+ "log": {"input": True, "output": False, "window": False},
+ "limit": {"rate": 3, "burst": 4, "action": "drop"},
+ "file": {"path": "/test/path/file"},
+ "syslog": {"facility": "testfac", "priority": "info"},
+ "journal": {"priority": "info", "augment": False},
+ "writer": "file",
+ }
+ ),
)
# SSSD config
@@ -228,13 +230,13 @@ class TestApplication(MachineCase):
# Check that the configs reflected the changes
conf = configparser.ConfigParser()
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"))
- assert conf["session_recording"]["scope"] == "some"
- assert conf["session_recording"]["users"] == "test users"
- assert conf["session_recording"]["groups"] == "test groups"
+ self.assertEqual(conf["session_recording"]["scope"], "some")
+ self.assertEqual(conf["session_recording"]["users"], "test users")
+ self.assertEqual(conf["session_recording"]["groups"], "test groups")
conf.read_file(open(test_all_file, "r"))
- assert conf["session_recording"]["scope"] == "all"
+ self.assertEqual(conf["session_recording"]["scope"], "all")
def testDisplayDrag(self):
b, _ = self._login()
@@ -248,23 +250,22 @@ class TestApplication(MachineCase):
b.click("#player-zoom-in")
# select and ensure drag'n'pan mode
b.click("#player-drag-pan")
- b.wait_present(".fa-hand-rock-o")
# scroll and check for screen movement
b.mouse(".dragnpan", "mousedown", 200, 200)
b.mouse(".dragnpan", "mousemove", 10, 10)
- assert b.attr(".dragnpan", "scrollTop") != 0
- assert b.attr(".dragnpan", "scrollLeft") != 0
+ self.assertNotEqual(b.attr(".dragnpan", "scrollTop"), 0)
+ self.assertNotEqual(b.attr(".dragnpan", "scrollLeft"), 0)
def testLogCorrelation(self):
b, _ = self._login()
# select the recording with the extra logs
- self._sel_rec(":contains('01:07')")
+ self._sel_rec(1)
b.click("#btn-logs-view")
# fast forward until the end
while "exit" not in b.text(self._term_line(22)):
b.click("#player-skip-frame")
# 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):
default_scale_sel = '.console-ct[style^="transform: scale(1)"]'
@@ -290,17 +291,20 @@ class TestApplication(MachineCase):
def _filter(self, inp, occ_dict):
import time
+ # allow temporary timestamp failures
+ self.allow_journal_messages(".*timestamp.*")
+ # login and test inputs
b, _ = self._login()
for occ in occ_dict:
for term in occ_dict[occ]:
# enter the search term and wait for the results to return
b.set_input_text(inp, term)
- time.sleep(0.5)
- assert b.text(".listing-ct").count("contractor") == occ
+ time.sleep(2)
+ self.assertEqual(b.text(".pf-c-table").count("contractor"), occ)
def testSearch(self):
self._filter(
- "#recording-search",
+ "#filter-search",
{
0: {
"this should return nothing",
@@ -327,7 +331,7 @@ class TestApplication(MachineCase):
def testFilterUsername(self):
self._filter(
- "#username-search",
+ "#filter-username",
{
0: {"test", "contact", "contractor", "contractor11", "contractor4"},
2: {"contractor1"},
@@ -336,7 +340,7 @@ class TestApplication(MachineCase):
def testFilterSince(self):
self._filter(
- "#since-search .bootstrap-datepicker",
+ "#filter-since",
{
0: {"2020-06-02", "2020-06-01 12:31:00"},
1: {"2020-06-01 12:17:01", "2020-06-01 12:30:50"},
@@ -346,10 +350,10 @@ class TestApplication(MachineCase):
def testFilterUntil(self):
self._filter(
- "#until-search .bootstrap-datepicker",
+ "#filter-until",
{
- 0: {"2020-06-01", "2020-06-01 12:16:00"},
- 1: {"2020-06-01 12:17:00", "2020-06-01 12:29:42"},
+ 0: {"2020-06-01", "2020-06-01 12:16"},
+ 1: {"2020-06-01 12:17", "2020-06-01 12:29"},
2: {"2020-06-02", "2020-06-01 12:31:00"},
},
)
diff --git a/webpack.config.js b/webpack.config.js
index aa85b51..7790fc5 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,49 +1,34 @@
const path = require("path");
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 webpack = require("webpack");
const CompressionPlugin = require("compression-webpack-plugin");
var externals = {
- "cockpit": "cockpit",
+ cockpit: "cockpit",
};
/* These can be overridden, typically from the Makefile.am */
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 section = process.env.ONLYDIR || null;
-const libdir = path.resolve(srcdir, "pkg" + path.sep + "lib");
-const nodedir = path.resolve((process.env.SRCDIR || __dirname), "node_modules");
+const libdir = path.resolve(srcdir, "pkg" + path.sep + "lib")
+const nodedir = path.resolve(process.env.SRCDIR || __dirname, "node_modules");
/* A standard nodejs and webpack pattern */
-var production = process.env.NODE_ENV === 'production';
+var production = process.env.NODE_ENV === "production";
var info = {
entries: {
- "recordings": [
- "./recordings.jsx",
- "./recordings.css",
- "./pkg/lib/listing.less",
+ index: [
+ "./index.js",
],
- "config": [
- "./config.jsx",
- "./recordings.css",
- "./table.css",
- ]
},
files: [
"index.html",
- "config.html",
- "player.jsx",
- "player.css",
- "recordings.jsx",
- "recordings.css",
- "table.css",
- "manifest.json",
- "timer.css",
- "./pkg/lib/listing.less",
+ "manifest.json"
],
};
@@ -64,108 +49,150 @@ var output = {
function vpath(/* ... */) {
var filename = Array.prototype.join.call(arguments, path.sep);
var expanded = builddir + path.sep + filename;
- if (fs.existsSync(expanded))
- return expanded;
+ if (fs.existsSync(expanded)) return expanded;
expanded = srcdir + path.sep + filename;
return expanded;
}
/* Qualify all the paths in entries */
-Object.keys(info.entries).forEach(function(key) {
+Object.keys(info.entries).forEach(function (key) {
if (section && key.indexOf(section) !== 0) {
delete info.entries[key];
return;
}
- info.entries[key] = info.entries[key].map(function(value) {
- if (value.indexOf("/") === -1)
- return value;
- else
- return vpath(value);
+ info.entries[key] = info.entries[key].map(function (value) {
+ if (value.indexOf("/") === -1) return value;
+ else return vpath(value);
});
});
/* Qualify all the paths in files listed */
var files = [];
-info.files.forEach(function(value) {
+info.files.forEach(function (value) {
if (!section || value.indexOf(section) === 0)
files.push({ from: vpath("src", value), to: value });
});
info.files = files;
-var plugins = [
- new copy(info.files),
- new extract("[name].css")
-];
+var plugins = [new copy(info.files), new extract({ filename: "[name].css" })];
/* Only minimize when in production mode */
if (production) {
/* Rename output files when minimizing */
output.filename = "[name].min.js";
- plugins.unshift(new CompressionPlugin({
- asset: "[path].gz[query]",
- test: /\.(js|html)$/,
- minRatio: 0.9,
- deleteOriginalAssets: true
- }));
+ plugins.unshift(
+ new CompressionPlugin({
+ asset: "[path].gz[query]",
+ test: /\.(js|html)$/,
+ minRatio: 0.9,
+ 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 = {
- mode: production ? 'production' : 'development',
+ mode: production ? "production" : "development",
+ resolve: {
+ modules: [libdir, nodedir],
+ },
entry: info.entries,
externals: externals,
output: output,
devtool: "source-map",
- resolve: {
- alias: {
- "fs": path.resolve(nodedir, "fs-extra"),
- },
- modules: [libdir, nodedir],
- },
module: {
rules: [
{
- enforce: 'pre',
+ enforce: "pre",
exclude: /node_modules/,
- loader: 'eslint-loader',
- test: /\.jsx$/
- },
- {
- enforce: 'pre',
- exclude: /node_modules/,
- loader: 'eslint-loader',
- test: /\.es6$/
+ loader: "eslint-loader",
+ test: /\.(js|jsx)$/,
},
{
exclude: /node_modules/,
- loader: 'babel-loader',
- test: /\.js$/
+ use: babel_loader,
+ 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,
+ },
+ },
+ {
+ loader: "string-replace-loader",
+ options: {
+ multiple: [
+ {
+ search: /src:url\("patternfly-icons-fake-path\/pficon[^}]*/g,
+ replace: "src:url('fonts/patternfly.woff')format('woff');",
+ },
+ {
+ search: /@font-face[^}]*patternfly-fonts-fake-path[^}]*}/g,
+ replace: "",
+ },
+ ],
+ },
+ },
+ {
+ loader: "sass-loader",
+ options: {
+ sourceMap: true,
+ outputStyle: "compressed",
+ },
+ },
+ ],
},
{
- exclude: /node_modules/,
- loader: 'babel-loader',
- test: /\.jsx$/
+ test: /\.s?css$/,
+ exclude: /patternfly-4-cockpit.scss/,
+ use: [
+ extract.loader,
+ {
+ loader: "css-loader",
+ options: {
+ sourceMap: true,
+ url: false,
+ },
+ },
+ {
+ loader: "sass-loader",
+ options: {
+ sourceMap: true,
+ outputStyle: "compressed",
+ },
+ },
+ ],
},
- {
- exclude: /node_modules/,
- loader: 'babel-loader',
- test: /\.es6$/
- },
- {
- test: /\.less$/,
- loader: extract.extract("css-loader!less-loader")
- },
- {
- exclude: /node_modules/,
- loader: extract.extract('css-loader!sass-loader'),
- test: /\.scss$/
- },
- {
- loader: extract.extract("css-loader?minimize=&root=" + libdir),
- test: /\.css$/,
- }
- ]
+ ],
},
- plugins: plugins
-}
+ plugins: plugins,
+};