Add Search

This commit is contained in:
Kyrylo Gliebov 2018-11-22 11:51:34 +01:00 committed by Kirill Glebov
parent 09039778c2
commit 66998cafa8
5 changed files with 301 additions and 124 deletions

View file

@ -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)

View file

@ -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,8 +1040,10 @@ export class Player extends React.Component {
"-": this.zoomOut,
"Z": this.fitIn,
};
if (keyCodesFuncs[event.key]) {
(keyCodesFuncs[event.key](event));
if (event.target.nodeName.toLowerCase() !== 'input') {
if (keyCodesFuncs[event.key]) {
(keyCodesFuncs[event.key](event));
}
}
}
@ -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,75 +1227,132 @@ export class Player extends React.Component {
// ensure react never reuses this div by keying it with the terminal widget
return (
<div id="recording-wrap">
<div className="col-md-6 player-wrap">
<div ref="wrapper" className="panel panel-default">
<div className="panel-heading">
<span>{this.state.title}</span>
</div>
<div className="panel-body">
<div className={(this.state.drag_pan ? "dragnpan" : "")} style={scrollwrap} ref="scrollwrap">
<div ref="term" className="console-ct" key={this.state.term} style={style} />
<React.Fragment>
<div className="row">
<div id="recording-wrap">
<div className="col-md-6 player-wrap">
<div ref="wrapper" className="panel panel-default">
<div className="panel-heading">
<span>{this.state.title}</span>
</div>
<div className="panel-body">
<div 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 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"
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"
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"
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"
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"
className="btn btn-default btn-lg" onClick={this.speedDown}>
/2
</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"
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"
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"
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"
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"
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 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"
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"
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"
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"
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"
className="btn btn-default btn-lg" onClick={this.speedDown}>
/2
</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"
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"
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"
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"
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"
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 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>
<ErrorList list={this.error_service.errors} />
</div>
</div>
</div>
</div>
</React.Fragment>
);
}

View file

@ -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;
}

View file

@ -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>
{player}
</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">

View file

@ -19,7 +19,6 @@
import Player from "./player";
"use strict";
var React = require("react");