/* * 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 . */ import cockpit from "cockpit"; import React from "react"; import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; import { ExclamationTriangleIcon, TimesCircleIcon } from '@patternfly/react-icons'; import { journal } from "journal"; import "journal.css"; import "cockpit-components-logs-panel.scss"; const _ = cockpit.gettext; /* JournalOutput implements the interface expected by journal.renderer, and also collects the output. */ export class JournalOutput { constructor(search_options) { this.logs = []; this.reboot_key = 0; this.search_options = search_options || {}; } onEvent(ev, cursor, full_content) { // only consider primary mouse button for clicks if (ev.type === 'click') { if (ev.button !== 0) return; // Ignore if text is being selected - less than 3 characters most likely means misclick const selection = window.getSelection().toString(); if (selection && selection.length > 2 && full_content.indexOf(selection) >= 0) return; } // only consider enter button for keyboard events if (ev.type === 'KeyDown' && ev.key !== "Enter") return; cockpit.jump("system/logs#/" + cursor + "?parent_options=" + JSON.stringify(this.search_options)); } render_line(ident, prio, message, count, time, entry) { let problem = false; let warning = false; if (ident === 'abrt-notification') { problem = true; ident = entry.PROBLEM_BINARY; } else if (prio < 4) { warning = true; } const full_content = [time, message, ident].join("\n"); return (
this.onEvent(ev, entry.__CURSOR, full_content)} onKeyDown={ev => this.onEvent(ev, entry.__CURSOR, full_content)}>
{ warning ? : null } { problem ? : null }
{time}
{message} { count > 1 ?
{ident}
{count}
:
{ident}
}
); } render_day_header(day) { return
{day}
; } render_reboot_separator() { return (
{_("Reboot")}
); } prepend(item) { this.logs.unshift(item); } append(item) { this.logs.push(item); } remove_first() { this.logs.shift(); } remove_last() { this.logs.pop(); } limit(max) { if (this.logs.length > max) this.logs = this.logs.slice(0, max); } } export class LogsPanel extends React.Component { constructor() { super(); this.state = { logs: [] }; } componentDidMount() { this.journalctl = journal.journalctl(this.props.match, { count: this.props.max }); const out = new JournalOutput(this.props.search_options); const render = journal.renderer(out); this.journalctl.stream((entries) => { for (let i = 0; i < entries.length; i++) render.prepend(entries[i]); render.prepend_flush(); // "max + 1" since there is always a date header and we // want to show "max" entries below it. out.limit(this.props.max + 1); this.setState({ logs: out.logs }); }); } componentWillUnmount() { this.journalctl.stop(); } render() { const actions = (this.state.logs.length > 0 && this.props.goto_url) && ; return ( {this.props.title} { this.state.logs.length ? this.state.logs : this.props.emptyMessage } ); } } LogsPanel.defaultProps = { emptyMessage: [], };