diff --git a/src/player.jsx b/src/player.jsx
index 929ab5d..b8a77f8 100644
--- a/src/player.jsx
+++ b/src/player.jsx
@@ -765,6 +765,9 @@
}
}
+ /* Send packet ts to the top */
+ this.props.onTsChange(this.pkt.pos);
+
/* Output the packet */
if (this.pkt.is_io && !this.pkt.is_output) {
this.sendInput(this.pkt);
diff --git a/src/recordings.css b/src/recordings.css
index e9293cf..2d27ab4 100644
--- a/src/recordings.css
+++ b/src/recordings.css
@@ -366,3 +366,19 @@ table.listing-ct > thead th:last-child, tr.listing-ct-item td:last-child {
-ms-user-select: none;
user-select: none;
}
+
+.highlighted {
+ background-color: #ededed !important;
+}
+
+#logs-view {
+ height: 300px;
+ overflow-y: scroll;
+ margin-bottom: 0;
+}
+
+.logs-view-log-time {
+ display: inline-block;
+ width: 150px;
+ vertical-align: middle;
+}
diff --git a/src/recordings.jsx b/src/recordings.jsx
index 3153ee4..25c7e79 100644
--- a/src/recordings.jsx
+++ b/src/recordings.jsx
@@ -243,6 +243,184 @@
}
}
+ function LogElement(props) {
+ const entry = props.entry;
+ const start = props.start;
+ const end = props.end;
+ const entry_timestamp = entry.__REALTIME_TIMESTAMP / 1000;
+ let className = 'cockpit-logline';
+ if (start < entry_timestamp && end > entry_timestamp) {
+ className = 'cockpit-logline highlighted';
+ }
+ return (
+
+
+
+
+
{formatDateTime(parseInt(entry.__REALTIME_TIMESTAMP / 1000))}
+
{entry.MESSAGE}
+
+ );
+ }
+
+ function LogsView(props) {
+ const entries = props.entries;
+ const start = props.start;
+ const end = props.end;
+ const rows = entries.map((entry) =>
+
+ );
+ return (
+
+ {rows}
+
+ );
+ }
+
+ let Logs = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.journalctlError = this.journalctlError.bind(this);
+ this.journalctlIngest = this.journalctlIngest.bind(this);
+ this.journalctlPrepend = this.journalctlPrepend.bind(this);
+ this.getLogs = this.getLogs.bind(this);
+ this.loadLater = this.loadLater.bind(this);
+ this.loadEarlier = this.loadEarlier.bind(this);
+ this.loadForTs = this.loadForTs.bind(this);
+ this.journalCtl = null;
+ this.entries = [];
+ this.start = null;
+ this.end = null;
+ this.earlier_than = null;
+ this.load_earlier = false;
+ this.state = {
+ cursor: null,
+ after: null,
+ entries: [],
+ };
+ }
+
+ 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));
+ }
+
+ journalctlIngest(entryList) {
+ if (this.load_earlier === true) {
+ entryList.push(...this.entries);
+ this.entries = entryList;
+ this.setState({entries: this.entries});
+ this.load_earlier = false;
+ this.scrollToTop();
+ } else {
+ 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();
+ }
+ }
+ }
+
+ journalctlPrepend(entryList) {
+ entryList.push(...this.entries);
+ this.setState({entries: this.entries});
+ }
+
+ getLogs() {
+ if (this.start != null && this.end != null) {
+ if (this.journalCtl != null) {
+ this.journalCtl.stop();
+ this.journalCtl = null;
+ }
+
+ let matches = [];
+
+ let options = {
+ since: formatDateTime(this.start),
+ until: formatDateTime(this.end),
+ follow: false,
+ count: "all",
+ };
+
+ if (this.load_earlier === true) {
+ options["until"] = formatDateTime(this.earlier_than);
+ } else if (this.state.after != null) {
+ options["after"] = this.state.after;
+ delete options.since;
+ }
+
+ const self = this;
+ this.journalCtl = Journal.journalctl(matches, options)
+ .fail(this.journalctlError)
+ .done(function(data) {
+ self.journalctlIngest(data);
+ });
+ }
+ }
+
+ loadEarlier() {
+ this.load_earlier = true;
+ this.start = this.start - 3600;
+ this.getLogs();
+ }
+
+ loadLater() {
+ this.start = this.end;
+ this.end = this.end + 3600;
+ this.getLogs();
+ }
+
+ loadForTs(ts) {
+ this.end = this.start + ts;
+ this.getLogs();
+ }
+
+ componentDidUpdate() {
+ if (this.props.recording) {
+ if (this.start === null && this.end === null) {
+ this.end = this.props.recording.start + 3600;
+ this.start = this.props.recording.start;
+ this.earlier_than = this.props.recording.start;
+ }
+ this.getLogs();
+ }
+ if (this.props.curTs) {
+ const ts = this.props.curTs;
+ this.loadForTs(ts);
+ }
+ }
+
+ render() {
+ if (this.props.recording) {
+ return (
+
+
+ Logs
+
+
+
+
+
+
+
+ );
+ } else {
+ return (Loading...
);
+ }
+ }
+ }
+
/*
* A component representing a single recording view.
* Properties:
@@ -303,7 +481,8 @@
let player =
();
+ matchList={this.props.recording.matchList}
+ onTsChange={this.props.onTsChange} />);
return (
@@ -548,6 +727,7 @@
this.handleDateUntilChange = this.handleDateUntilChange.bind(this);
this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.handleHostnameChange = this.handleHostnameChange.bind(this);
+ this.handleTsChange = this.handleTsChange.bind(this);
/* Journalctl instance */
this.journalctl = null;
/* Recording ID journalctl instance is invoked with */
@@ -570,6 +750,7 @@
hostname: cockpit.location.options.hostname || null,
error_tlog_uid: false,
diff_hosts: false,
+ curTs: null,
}
}
@@ -766,6 +947,10 @@
cockpit.location.go([], $.extend(cockpit.location.options, { hostname: hostname }));
}
+ handleTsChange(ts) {
+ this.setState({curTs: ts});
+ }
+
componentDidMount() {
let proc = cockpit.spawn(["getent", "passwd", "tlog"]);
@@ -842,7 +1027,16 @@
);
} else {
return (
-
+
);
}
}