Line data Source code
1 : /*
2 : * Copyright (C) 2017 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 53 : import cockpit from "cockpit";
7 53 : 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 53 : const _ = cockpit.gettext;
19 :
20 : /* JournalOutput implements the interface expected by
21 : journal.renderer, and also collects the output.
22 : */
23 :
24 53 : export class JournalOutput {
25 34 : constructor(search_options) {
26 34 : this.logs = [];
27 34 : this.reboot_key = 0;
28 3 : this.search_options = search_options || {};
29 34 : }
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 4 : ? <div className="cockpit-log-service-container" role="cell">
75 4 : <div className="cockpit-log-service-reduced">{ident}</div>
76 4 : <Badge screenReaderText={_("Occurrences")} isRead key={count}>{count}</Badge>
77 4 : </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 5 : render_reboot_separator() {
89 5 : return (
90 5 : <div className="cockpit-logline" role="row" key={"reboot-" + this.reboot_key++}>
91 5 : <div className="cockpit-log-warning" role="cell" />
92 5 : <span className="cockpit-log-message cockpit-logmsg-reboot" role="cell">{_("Reboot")}</span>
93 5 : </div>
94 : );
95 5 : }
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 53 : }
118 :
119 53 : 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 11 : componentWillUnmount() {
143 11 : this.journalctl.stop();
144 11 : }
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 53 : }
161 53 : LogsPanel.defaultProps = {
162 53 : emptyMessage: [],
163 53 : };
|