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-06-16 14:09:37

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2017 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6           51 : import cockpit from "cockpit";
       7           51 : 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           51 : const _ = cockpit.gettext;
      19              : 
      20              : /* JournalOutput implements the interface expected by
      21              :    journal.renderer, and also collects the output.
      22              :  */
      23              : 
      24           51 : export class JournalOutput {
      25           33 :     constructor(search_options) {
      26           33 :         this.logs = [];
      27           33 :         this.reboot_key = 0;
      28            3 :         this.search_options = search_options || {};
      29           33 :     }
      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           24 :     render_line(ident, prio, message, count, time, entry) {
      51           24 :         let warning = false;
      52              : 
      53            8 :         if (prio < 4) {
      54            8 :             warning = true;
      55            8 :         }
      56              : 
      57           24 :         const full_content = [time, message, ident].join("\n");
      58              : 
      59           24 :         return (
      60           24 :             <div className="cockpit-logline" role="row" tabIndex={0} key={entry.__CURSOR}
      61           24 :                 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           24 :                 <div className="cockpit-log-warning" role="cell">
      65           24 :                     { warning
      66            8 :                         ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
      67           23 :                         : null
      68              :                     }
      69           24 :                 </div>
      70           24 :                 <div className="cockpit-log-time" role="cell">{time}</div>
      71           24 :                 <span className="cockpit-log-message" role="cell">{message}</span>
      72              :                 {
      73           24 :                     count > 1
      74            3 :                         ? <div className="cockpit-log-service-container" role="cell">
      75            3 :                             <div className="cockpit-log-service-reduced">{ident}</div>
      76            3 :                             <Badge screenReaderText={_("Occurrences")} isRead key={count}>{count}</Badge>
      77            3 :                         </div>
      78           24 :                         : <div className="cockpit-log-service" role="cell">{ident}</div>
      79              :                 }
      80           24 :             </div>
      81              :         );
      82           24 :     }
      83              : 
      84           23 :     render_day_header(day) {
      85           23 :         return <div className="panel-heading" key={day}>{day}</div>;
      86           23 :     }
      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           23 :     prepend(item) {
      98           23 :         this.logs.unshift(item);
      99           23 :     }
     100              : 
     101            9 :     append(item) {
     102            9 :         this.logs.push(item);
     103            9 :     }
     104              : 
     105           14 :     remove_first() {
     106           14 :         this.logs.shift();
     107           14 :     }
     108              : 
     109            5 :     remove_last() {
     110            5 :         this.logs.pop();
     111            5 :     }
     112              : 
     113           14 :     limit(max) {
     114           14 :         if (this.logs.length > max)
     115            2 :             this.logs = this.logs.slice(0, max);
     116           14 :     }
     117           51 : }
     118              : 
     119           51 : export class LogsPanel extends React.Component {
     120           23 :     constructor() {
     121           23 :         super();
     122           23 :         this.state = { logs: [] };
     123           23 :     }
     124              : 
     125           23 :     componentDidMount() {
     126           23 :         this.journalctl = journal.journalctl(this.props.match, { count: this.props.max });
     127              : 
     128           23 :         const out = new JournalOutput(this.props.search_options);
     129           23 :         const render = journal.renderer(out);
     130              : 
     131           14 :         this.journalctl.stream((entries) => {
     132           14 :             for (let i = 0; i < entries.length; i++)
     133           14 :                 render.prepend(entries[i]);
     134           14 :             render.prepend_flush();
     135              :             // "max + 1" since there is always a date header and we
     136              :             // want to show "max" entries below it.
     137           14 :             out.limit(this.props.max + 1);
     138           14 :             this.setState({ logs: out.logs });
     139           14 :         });
     140           23 :     }
     141              : 
     142           12 :     componentWillUnmount() {
     143           12 :         this.journalctl.stop();
     144           12 :     }
     145              : 
     146           23 :     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           23 :         return (
     150           23 :             <Card isPlain className="cockpit-log-panel">
     151           23 :                 <CardHeader actions={{ actions }}>
     152           23 :                     <CardTitle>{this.props.title}</CardTitle>
     153           23 :                 </CardHeader>
     154           15 :                 <CardBody className={(!this.state.logs.length && this.props.emptyMessage.length) ? "empty-message" : "contains-list"}>
     155           15 :                     { this.state.logs.length ? this.state.logs : this.props.emptyMessage }
     156           23 :                 </CardBody>
     157           23 :             </Card>
     158              :         );
     159           23 :     }
     160           51 : }
     161           51 : LogsPanel.defaultProps = {
     162           51 :     emptyMessage: [],
     163           51 : };
        

Generated by: LCOV version 2.0-1