Add Search
This commit is contained in:
parent
09039778c2
commit
66998cafa8
5 changed files with 301 additions and 124 deletions
|
|
@ -119,6 +119,8 @@
|
|||
cmd.push("--after=" + options.after);
|
||||
if (options.merge)
|
||||
cmd.push("-m");
|
||||
if (options.grep)
|
||||
cmd.push("--grep=" + options.grep);
|
||||
|
||||
/* journalctl doesn't allow reverse and follow together */
|
||||
if (options.reverse)
|
||||
|
|
|
|||
208
src/player.jsx
208
src/player.jsx
|
|
@ -20,12 +20,53 @@
|
|||
import React from 'react';
|
||||
let cockpit = require("cockpit");
|
||||
let _ = cockpit.gettext;
|
||||
let moment = require("moment");
|
||||
let Term = require("term.js-cockpit");
|
||||
let Journal = require("journal");
|
||||
let $ = require("jquery");
|
||||
require("console.css");
|
||||
require("bootstrap-slider");
|
||||
|
||||
let padInt = function (n, w) {
|
||||
let i = Math.floor(n);
|
||||
let a = Math.abs(i);
|
||||
let s = a.toString();
|
||||
for (w -= s.length; w > 0; w--) {
|
||||
s = '0' + s;
|
||||
}
|
||||
return ((i < 0) ? '-' : '') + s;
|
||||
};
|
||||
|
||||
let formatDateTime = function (ms) {
|
||||
return moment(ms).format("YYYY-MM-DD HH:mm:ss");
|
||||
};
|
||||
|
||||
/*
|
||||
* Format a time interval from a number of milliseconds.
|
||||
*/
|
||||
let formatDuration = function (ms) {
|
||||
let v = Math.floor(ms / 1000);
|
||||
let s = Math.floor(v % 60);
|
||||
v = Math.floor(v / 60);
|
||||
let m = Math.floor(v % 60);
|
||||
v = Math.floor(v / 60);
|
||||
let h = Math.floor(v % 24);
|
||||
let d = Math.floor(v / 24);
|
||||
let str = '';
|
||||
|
||||
if (d > 0) {
|
||||
str += d + ' ' + _("days") + ' ';
|
||||
}
|
||||
|
||||
if (h > 0 || str.length > 0) {
|
||||
str += padInt(h, 2) + ':';
|
||||
}
|
||||
|
||||
str += padInt(m, 2) + ':' + padInt(s, 2);
|
||||
|
||||
return (ms < 0 ? '-' : '') + str;
|
||||
};
|
||||
|
||||
let scrollToBottom = function(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
|
|
@ -564,6 +605,88 @@ class Slider extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
function SearchEntry(props) {
|
||||
return (
|
||||
<span className="search-result"><a onClick={(e) => props.fastForwardToTS(props.pos, e)}>{formatDuration(props.pos)}</a></span>
|
||||
);
|
||||
}
|
||||
|
||||
class Search extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleStream = this.handleStream.bind(this);
|
||||
this.handleError = this.handleError.bind(this);
|
||||
this.handleSearchSubmit = this.handleSearchSubmit.bind(this);
|
||||
this.clearSearchResults = this.clearSearchResults.bind(this);
|
||||
this.state = {
|
||||
search: cockpit.location.options.search_rec || cockpit.location.options.search || "",
|
||||
};
|
||||
}
|
||||
|
||||
handleInputChange(event) {
|
||||
event.preventDefault();
|
||||
const name = event.target.name;
|
||||
const value = event.target.value;
|
||||
let state = {};
|
||||
state[name] = value;
|
||||
this.setState(state);
|
||||
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});
|
||||
this.journalctl.fail(this.handleError);
|
||||
this.journalctl.stream(this.handleStream);
|
||||
}
|
||||
|
||||
handleStream(data) {
|
||||
let items = data.map(item => {
|
||||
return JSON.parse(item.MESSAGE);
|
||||
});
|
||||
items = items.map(item => {
|
||||
return <SearchEntry key={item.id} fastForwardToTS={this.props.fastForwardToTS} pos={item.pos} />;
|
||||
});
|
||||
this.setState({items: items});
|
||||
}
|
||||
|
||||
handleError(data) {
|
||||
this.props.errorService.addMessage(data);
|
||||
}
|
||||
|
||||
clearSearchResults() {
|
||||
delete cockpit.location.options.search;
|
||||
cockpit.location.go(cockpit.location.path[0], cockpit.location.options);
|
||||
this.setState({search: ""});
|
||||
this.handleStream([]);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.state.search) {
|
||||
this.handleSearchSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="search-wrap">
|
||||
<div className="input-group search-component">
|
||||
<input type="text" className="form-control" name="search" value={this.state.search} onChange={this.handleInputChange} />
|
||||
<span className="input-group-btn">
|
||||
<button className="btn btn-default" onClick={this.handleSearchSubmit}><span className="glyphicon glyphicon-search" /></button>
|
||||
<button className="btn btn-default" onClick={this.clearSearchResults}><span className="glyphicon glyphicon-remove" /></button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="search-results">
|
||||
{this.state.items}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InputPlayer extends React.Component {
|
||||
render() {
|
||||
const input = String(this.props.input).replace(/(?:\r\n|\r|\n)/g, " ");
|
||||
|
|
@ -905,7 +1028,7 @@ export class Player extends React.Component {
|
|||
|
||||
handleKeyDown(event) {
|
||||
let keyCodesFuncs = {
|
||||
"p": this.playPauseToggle,
|
||||
"P": this.playPauseToggle,
|
||||
"}": this.speedUp,
|
||||
"{": this.speedDown,
|
||||
"Backspace": this.speedReset,
|
||||
|
|
@ -917,10 +1040,12 @@ export class Player extends React.Component {
|
|||
"-": this.zoomOut,
|
||||
"Z": this.fitIn,
|
||||
};
|
||||
if (event.target.nodeName.toLowerCase() !== 'input') {
|
||||
if (keyCodesFuncs[event.key]) {
|
||||
(keyCodesFuncs[event.key](event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zoom(scale) {
|
||||
if (scale.toFixed(6) === this.state.scale_initial.toFixed(6)) {
|
||||
|
|
@ -1064,6 +1189,8 @@ export class Player extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
let r = this.props.recording;
|
||||
|
||||
let speedExp = this.state.speedExp;
|
||||
let speedFactor = Math.pow(2, Math.abs(speedExp));
|
||||
let speedStr;
|
||||
|
|
@ -1100,6 +1227,8 @@ export class Player extends React.Component {
|
|||
|
||||
// ensure react never reuses this div by keying it with the terminal widget
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="row">
|
||||
<div id="recording-wrap">
|
||||
<div className="col-md-6 player-wrap">
|
||||
<div ref="wrapper" className="panel panel-default">
|
||||
|
|
@ -1113,62 +1242,117 @@ export class Player extends React.Component {
|
|||
</div>
|
||||
<div className="panel-footer">
|
||||
<Slider length={this.buf.pos} mark={this.state.currentTsPost} fastForwardFunc={this.fastForwardToTS} play={this.play} pause={this.pause} paused={this.state.paused} />
|
||||
<button title={_("Play/Pause - Hotkey: p")} type="button" ref="playbtn"
|
||||
<button 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 title={_("Skip Frame - Hotkey: .")} type="button"
|
||||
<button 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 title={_("Restart Playback - Hotkey: Shift-R")} type="button"
|
||||
<button 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 title={_("Fast-forward to end - Hotkey: Shift-G")} type="button"
|
||||
<button 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 title={_("Speed /2 - Hotkey: {")} type="button"
|
||||
<button title="Speed /2 - Hotkey: {" type="button"
|
||||
className="btn btn-default btn-lg" onClick={this.speedDown}>
|
||||
/2
|
||||
</button>
|
||||
<button title={_("Reset Speed - Hotkey: Backspace")} type="button"
|
||||
<button title="Reset Speed - Hotkey: Backspace" type="button"
|
||||
className="btn btn-default btn-lg" onClick={this.speedReset}>
|
||||
1:1
|
||||
</button>
|
||||
<button title={_("Speed x2 - Hotkey: }")} type="button"
|
||||
<button title="Speed x2 - Hotkey: }" type="button"
|
||||
className="btn btn-default btn-lg margin-right-btn"
|
||||
onClick={this.speedUp}>
|
||||
x2
|
||||
</button>
|
||||
<span>{speedStr}</span>
|
||||
<span style={to_right}>
|
||||
<button title={_("Drag'n'Pan")} type="button" className="btn btn-default btn-lg"
|
||||
<button 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 title={_("Zoom In - Hotkey: =")} type="button" className="btn btn-default btn-lg"
|
||||
<button 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 title={_("Fit To - Hotkey: Z")} type="button" className="btn btn-default btn-lg"
|
||||
<button 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 title={_("Zoom Out - Hotkey: -")} type="button" className="btn btn-default btn-lg"
|
||||
<button 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>
|
||||
<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-6">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -401,3 +401,24 @@ table.listing-ct > thead th:last-child, tr.listing-ct-item td:last-child {
|
|||
.panel-footer {
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
margin-left: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
float: left;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
.search-component {
|
||||
float: left;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
min-height: 25px;
|
||||
display:block;
|
||||
clear:both;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -404,6 +404,9 @@ class Recording extends React.Component {
|
|||
|
||||
goBackToList() {
|
||||
if (cockpit.location.path[0]) {
|
||||
if ("search_rec" in cockpit.location.options) {
|
||||
delete cockpit.location.options.search_rec;
|
||||
}
|
||||
cockpit.location.go([], cockpit.location.options);
|
||||
} else {
|
||||
cockpit.location.go('/');
|
||||
|
|
@ -420,7 +423,9 @@ class Recording extends React.Component {
|
|||
ref="player"
|
||||
matchList={this.props.recording.matchList}
|
||||
logsTs={this.props.logsTs}
|
||||
onTsChange={this.props.onTsChange} />);
|
||||
search={this.props.search}
|
||||
onTsChange={this.props.onTsChange}
|
||||
recording={r} />);
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
|
|
@ -432,58 +437,7 @@ class Recording extends React.Component {
|
|||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
{player}
|
||||
<div className="col-md-6">
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -645,6 +599,7 @@ class View extends React.Component {
|
|||
date_until: cockpit.location.options.date_until || "",
|
||||
username: cockpit.location.options.username || "",
|
||||
hostname: cockpit.location.options.hostname || "",
|
||||
search: cockpit.location.options.search || "",
|
||||
/* filter values end */
|
||||
error_tlog_uid: false,
|
||||
diff_hosts: false,
|
||||
|
|
@ -671,6 +626,7 @@ class View extends React.Component {
|
|||
date_until: cockpit.location.options.date_until || "",
|
||||
username: cockpit.location.options.username || "",
|
||||
hostname: cockpit.location.options.hostname || "",
|
||||
search: cockpit.location.options.search || "",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -763,7 +719,7 @@ class View extends React.Component {
|
|||
* Assumes journalctl is not running.
|
||||
*/
|
||||
journalctlStart() {
|
||||
let matches = ["_UID=" + this.uid, "+", "_EXE=/usr/bin/tlog-rec-session", "+", "_EXE=/usr/bin/tlog-rec", "+", "SYSLOG_IDENTIFIER=\"-tlog-rec-session\""];
|
||||
let matches = ["_UID=" + this.uid, "+", "_EXE=/usr/bin/tlog-rec-session", "+", "_EXE=/usr/bin/tlog-rec", "+", "SYSLOG_IDENTIFIER=-tlog-rec-session"];
|
||||
if (this.state.username && this.state.username !== "") {
|
||||
matches.push("TLOG_USER=" + this.state.username);
|
||||
}
|
||||
|
|
@ -781,7 +737,12 @@ class View extends React.Component {
|
|||
options['until'] = this.state.date_until;
|
||||
}
|
||||
|
||||
if (this.state.search && this.state.search !== "" && this.state.recordingID === null) {
|
||||
options["grep"] = this.state.search;
|
||||
}
|
||||
|
||||
if (this.state.recordingID !== null) {
|
||||
delete options["grep"];
|
||||
matches.push("TLOG_REC=" + this.state.recordingID);
|
||||
}
|
||||
|
||||
|
|
@ -894,7 +855,8 @@ class View extends React.Component {
|
|||
if (this.state.date_since !== prevState.date_since ||
|
||||
this.state.date_until !== prevState.date_until ||
|
||||
this.state.username !== prevState.username ||
|
||||
this.state.hostname !== prevState.hostname
|
||||
this.state.hostname !== prevState.hostname ||
|
||||
this.state.search !== prevState.search
|
||||
) {
|
||||
this.clearRecordings();
|
||||
this.journalctlRestart();
|
||||
|
|
@ -929,7 +891,16 @@ class View extends React.Component {
|
|||
<Datetimepicker value={this.state.date_until} onChange={this.handleDateUntilChange} />
|
||||
</td>
|
||||
<td className="top">
|
||||
<label className="control-label" htmlFor="username">{_("Username")}</label>
|
||||
<label className="control-label" htmlFor="search">Search</label>
|
||||
</td>
|
||||
<td>
|
||||
<div className="input-group">
|
||||
<input type="text" 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">
|
||||
|
|
@ -972,7 +943,7 @@ class View extends React.Component {
|
|||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Recording recording={this.recordingMap[this.state.recordingID]} onTsChange={this.handleTsChange} logsTs={this.state.logsTs} />
|
||||
<Recording recording={this.recordingMap[this.state.recordingID]} onTsChange={this.handleTsChange} logsTs={this.state.logsTs} search={this.state.search} />
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
import Player from "./player";
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
var React = require("react");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue