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