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