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 : };
|