LCOV - code coverage report
Current view: top level - pkg/lib - cockpit-components-logs-panel.jsx Coverage Total Hit
Test: cockpit Lines: 97.3 % 110 107
Test Date: 2026-07-13 10:00:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2017 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          205 : import cockpit from "cockpit";
       7          205 : import React from "react";
       8              : 
       9              : import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
      10              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      11              : import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
      12              : import { ExclamationTriangleIcon } from '@patternfly/react-icons';
      13              : 
      14              : import { journal } from "journal";
      15              : import "journal.css";
      16              : import "cockpit-components-logs-panel.scss";
      17              : 
      18          205 : const _ = cockpit.gettext;
      19              : 
      20              : /* JournalOutput implements the interface expected by
      21              :    journal.renderer, and also collects the output.
      22              :  */
      23              : 
      24          205 : export class JournalOutput {
      25          167 :     constructor(search_options) {
      26          167 :         this.logs = [];
      27          167 :         this.reboot_key = 0;
      28           17 :         this.search_options = search_options || {};
      29          167 :     }
      30              : 
      31            5 :     onEvent(ev, cursor, full_content) {
      32              :         // only consider primary mouse button for clicks
      33            5 :         if (ev.type === 'click') {
      34            5 :             if (ev.button !== 0)
      35            5 :                 return;
      36              : 
      37              :             // Ignore if text is being selected - less than 3 characters most likely means misclick
      38            5 :             const selection = window.getSelection().toString();
      39            0 :             if (selection && selection.length > 2 && full_content.indexOf(selection) >= 0)
      40            5 :                 return;
      41            5 :         }
      42              : 
      43              :         // only consider enter button for keyboard events
      44            0 :         if (ev.type === 'KeyDown' && ev.key !== "Enter")
      45            5 :             return;
      46              : 
      47            5 :         cockpit.jump("system/logs#/" + cursor + "?parent_options=" + JSON.stringify(this.search_options));
      48            5 :     }
      49              : 
      50          123 :     render_line(ident, prio, message, count, time, entry) {
      51          123 :         let warning = false;
      52              : 
      53           20 :         if (prio < 4) {
      54           20 :             warning = true;
      55           20 :         }
      56              : 
      57          123 :         const full_content = [time, message, ident].join("\n");
      58              : 
      59          123 :         return (
      60          123 :             <div className="cockpit-logline" role="row" tabIndex={0} key={entry.__CURSOR}
      61          123 :                 data-cursor={entry.__CURSOR}
      62            5 :                 onClick={ev => this.onEvent(ev, entry.__CURSOR, full_content)}
      63            0 :                 onKeyDown={ev => this.onEvent(ev, entry.__CURSOR, full_content)}>
      64          123 :                 <div className="cockpit-log-warning" role="cell">
      65          123 :                     { warning
      66           20 :                         ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
      67          121 :                         : null
      68              :                     }
      69          123 :                 </div>
      70          123 :                 <div className="cockpit-log-time" role="cell">{time}</div>
      71          123 :                 <span className="cockpit-log-message" role="cell">{message}</span>
      72              :                 {
      73          123 :                     count > 1
      74           26 :                         ? <div className="cockpit-log-service-container" role="cell">
      75           26 :                             <div className="cockpit-log-service-reduced">{ident}</div>
      76           26 :                             <Badge screenReaderText={_("Occurrences")} isRead key={count}>{count}</Badge>
      77           26 :                         </div>
      78          123 :                         : <div className="cockpit-log-service" role="cell">{ident}</div>
      79              :                 }
      80          123 :             </div>
      81              :         );
      82          123 :     }
      83              : 
      84          122 :     render_day_header(day) {
      85          122 :         return <div className="panel-heading" key={day}>{day}</div>;
      86          122 :     }
      87              : 
      88            1 :     render_reboot_separator() {
      89            1 :         return (
      90            1 :             <div className="cockpit-logline" role="row" key={"reboot-" + this.reboot_key++}>
      91            1 :                 <div className="cockpit-log-warning" role="cell" />
      92            1 :                 <span className="cockpit-log-message cockpit-logmsg-reboot" role="cell">{_("Reboot")}</span>
      93            1 :             </div>
      94              :         );
      95            1 :     }
      96              : 
      97          121 :     prepend(item) {
      98          121 :         this.logs.unshift(item);
      99          121 :     }
     100              : 
     101           11 :     append(item) {
     102           11 :         this.logs.push(item);
     103           11 :     }
     104              : 
     105           42 :     remove_first() {
     106           42 :         this.logs.shift();
     107           42 :     }
     108              : 
     109            5 :     remove_last() {
     110            5 :         this.logs.pop();
     111            5 :     }
     112              : 
     113          111 :     limit(max) {
     114          111 :         if (this.logs.length > max)
     115           33 :             this.logs = this.logs.slice(0, max);
     116          111 :     }
     117          205 : }
     118              : 
     119          205 : export class LogsPanel extends React.Component {
     120          155 :     constructor() {
     121          155 :         super();
     122          155 :         this.state = { logs: [] };
     123          155 :     }
     124              : 
     125          155 :     componentDidMount() {
     126          155 :         this.journalctl = journal.journalctl(this.props.match, { count: this.props.max });
     127              : 
     128          155 :         const out = new JournalOutput(this.props.search_options);
     129          155 :         const render = journal.renderer(out);
     130              : 
     131          111 :         this.journalctl.stream((entries) => {
     132          111 :             for (let i = 0; i < entries.length; i++)
     133          111 :                 render.prepend(entries[i]);
     134          111 :             render.prepend_flush();
     135              :             // "max + 1" since there is always a date header and we
     136              :             // want to show "max" entries below it.
     137          111 :             out.limit(this.props.max + 1);
     138          111 :             this.setState({ logs: out.logs });
     139          111 :         });
     140          155 :     }
     141              : 
     142          121 :     componentWillUnmount() {
     143          121 :         this.journalctl.stop();
     144          121 :     }
     145              : 
     146          155 :     render() {
     147            2 :         const actions = (this.state.logs.length > 0 && this.props.goto_url) && <Button variant="secondary" onClick={e => cockpit.jump(this.props.goto_url)}>{_("View all logs")}</Button>;
     148              : 
     149          155 :         return (
     150          155 :             <Card isPlain className="cockpit-log-panel">
     151          155 :                 <CardHeader actions={{ actions }}>
     152          155 :                     <CardTitle>{this.props.title}</CardTitle>
     153          155 :                 </CardHeader>
     154           30 :                 <CardBody className={(!this.state.logs.length && this.props.emptyMessage.length) ? "empty-message" : "contains-list"}>
     155          115 :                     { this.state.logs.length ? this.state.logs : this.props.emptyMessage }
     156          155 :                 </CardBody>
     157          155 :             </Card>
     158              :         );
     159          155 :     }
     160          205 : }
     161          205 : LogsPanel.defaultProps = {
     162          205 :     emptyMessage: [],
     163          205 : };
        

Generated by: LCOV version 2.0-1