/* * This file is part of Cockpit. * * Copyright (C) 2017 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ "use strict"; import React from "react"; import ReactDOM from "react-dom"; 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"); /* * 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); let s = a.toString(); for (w -= s.length; w > 0; w--) { s = '0' + s; } return ((i < 0) ? '-' : '') + s; }; /* * Format date and time for a number of milliseconds since Epoch. */ let formatDateTime = function (ms) { let d = new Date(ms); return ( padInt(d.getFullYear(), 4) + '-' + padInt(d.getMonth() + 1, 2) + '-' + padInt(d.getDate(), 2) + ' ' + padInt(d.getHours(), 2) + ':' + padInt(d.getMinutes(), 2) + ':' + padInt(d.getSeconds(), 2) ); }; /* * 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 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() .trim(); 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 ts = entry_timestamp - start; if (ts > 0) { props.jumpToTs(ts); } else { props.jumpToTs(0); } }; const messageClick = () => { const url = '/system/logs#/' + cursor + '?parent_options={}'; const win = window.open(url, '_blank'); win.focus(); }; let className = 'cockpit-logline'; if (start < entry_timestamp && end > entry_timestamp) { className = 'cockpit-logline highlighted'; } return (
{formatDateTime(entry_timestamp)}
{entry.MESSAGE}
); } function LogsView(props) { const entries = props.entries; const start = props.start; const end = props.end; const rows = entries.map((entry) => ); return (
{rows}
); } class Logs 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: * - recording: either null for no recording data available yet, or a * recording object, as created by the View below. */ class Recording extends React.Component { constructor(props) { super(props); this.goBackToList = this.goBackToList.bind(this); } goBackToList() { if (cockpit.location.path[0]) { cockpit.location.go([], cockpit.location.options); } else { cockpit.location.go('/'); } } render() { let r = this.props.recording; if (r == null) { return Loading...; } else { let player = (); return (
{_("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}
{player}
); } } } /* * A component representing a list of recordings. * Properties: * - list: an array with recording objects, as created by the View below */ 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.state = { sorting_field: "start", sorting_asc: true, }; } drawSortDir() { $('#sort_arrow').remove(); let type = this.state.sorting_asc ? "asc" : "desc"; let arrow = '