Line data Source code
1 : /*
2 : * Copyright (C) 2020 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 10 : import cockpit from "cockpit";
7 : import { journal } from "journal";
8 : import * as timeformat from "timeformat";
9 :
10 10 : import React from 'react';
11 : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
12 : import { ExclamationCircleIcon } from '@patternfly/react-icons';
13 : import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
14 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
15 : import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
16 : import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
17 : import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
18 : import { Gallery, GalleryItem } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
19 :
20 10 : const _ = cockpit.gettext;
21 :
22 5 : const LogDetails = ({ entry }) => {
23 5 : const general = Object.keys(entry).filter(k => k !== 'MESSAGE');
24 5 : general.sort();
25 :
26 0 : const id = entry.UNIT || entry.SYSLOG_IDENTIFIER || "";
27 4 : let service = entry.USER_UNIT || entry.COREDUMP_USER_UNIT || entry._SYSTEMD_USER_UNIT || "";
28 5 : const is_user = !!service;
29 0 : service = service || entry.UNIT || entry.COREDUMP_UNIT || entry._SYSTEMD_UNIT || "";
30 :
31 : // Only show redirect for unit types we show
32 5 : if (["service", "target", "socket", "timer", "path"].indexOf(service.split(".").slice(-1)[0]) === -1)
33 2 : service = undefined;
34 :
35 5 : const actions = service && (
36 0 : <Button variant="link" onClick={() => cockpit.jump("/system/services#/" + service + (is_user ? "?owner=user" : "")) }>
37 3 : {cockpit.format(_("Go to $0"), service)}
38 3 : </Button>
39 : );
40 :
41 5 : return (
42 5 : <GalleryItem>
43 5 : <Card isPlain>
44 5 : <CardHeader actions={{ actions }}>
45 5 : <h2 id="entry-heading">{id}</h2>
46 5 : </CardHeader>
47 5 : <CardTitle>{journal.printable(entry.MESSAGE, "MESSAGE")}</CardTitle>
48 5 : <CardBody>
49 5 : <DescriptionList className="pf-m-horizontal-on-sm">
50 5 : { general.map(key =>
51 5 : <DescriptionListGroup key={key}>
52 5 : <DescriptionListTerm>{key}</DescriptionListTerm>
53 5 : <DescriptionListDescription data-label={key}>{journal.printable(entry[key], key)}</DescriptionListDescription>
54 5 : </DescriptionListGroup>
55 5 : )}
56 5 : </DescriptionList>
57 5 : </CardBody>
58 5 : </Card>
59 5 : </GalleryItem>
60 : );
61 5 : };
62 :
63 10 : export class LogEntry extends React.Component {
64 5 : constructor(props) {
65 5 : super(props);
66 5 : this.state = {
67 5 : error: "",
68 5 : entry: null,
69 5 : loading: true,
70 5 : };
71 :
72 5 : this.goHome = this.goHome.bind(this);
73 5 : }
74 :
75 5 : componentDidMount() {
76 5 : const cursor = cockpit.location.path[0];
77 5 : journal.journalctl({ cursor, count: 1, follow: false })
78 5 : .then(entries => {
79 5 : if (entries.length >= 1 && entries[0].__CURSOR == cursor) {
80 5 : this.setState({ entry: entries[0], loading: false, error: "" });
81 5 : } else
82 0 : this.setState({ entry: null, loading: false, error: _("Journal entry not found") });
83 5 : })
84 0 : .catch(error => this.setState({ entry: null, loading: false, error }));
85 5 : }
86 :
87 4 : goHome(ev) {
88 4 : ev.preventDefault();
89 4 : let parent_options = {};
90 4 : if (cockpit.location.options.parent_options)
91 4 : parent_options = JSON.parse(cockpit.location.options.parent_options);
92 4 : cockpit.location.go('/', parent_options);
93 4 : }
94 :
95 5 : render() {
96 5 : let breadcrumb = _("Journal entry");
97 5 : let content = null;
98 :
99 5 : if (this.state.error)
100 0 : content = <EmptyStatePanel icon={ExclamationCircleIcon} title={this.state.error} />;
101 5 : else if (this.state.loading)
102 5 : content = <EmptyStatePanel loading title={ _("Loading...") } />;
103 5 : else if (this.state.entry) {
104 5 : const entry = this.state.entry;
105 5 : const date = timeformat.dateTimeSeconds(entry.__REALTIME_TIMESTAMP / 1000);
106 :
107 5 : breadcrumb = cockpit.format(_("Entry at $0"), date);
108 5 : content = <LogDetails entry={entry} />;
109 5 : }
110 :
111 5 : return (
112 5 : <Page id="log-details" className="log-details pf-m-no-sidebar">
113 5 : <PageBreadcrumb hasBodyWrapper={false} stickyOnBreakpoint={{ default: "top" }}>
114 5 : <Breadcrumb>
115 5 : <BreadcrumbItem onClick={this.goHome} className="pf-v6-c-breadcrumb__link">{_("Logs")}</BreadcrumbItem>
116 5 : <BreadcrumbItem isActive>
117 5 : {breadcrumb}
118 5 : </BreadcrumbItem>
119 5 : </Breadcrumb>
120 5 : </PageBreadcrumb>
121 5 : <PageSection hasBodyWrapper={false}>
122 5 : <Gallery hasGutter>
123 5 : {content}
124 5 : </Gallery>
125 5 : </PageSection>
126 5 : </Page>
127 : );
128 5 : }
129 10 : }
|