LCOV - code coverage report
Current view: top level - pkg/systemd - logDetails.jsx Coverage Total Hit
Test: cockpit Lines: 93.7 % 95 89
Test Date: 2026-07-17 12:03:54

            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              : 
      20           11 : 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           11 : 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           11 : }
        

Generated by: LCOV version 2.0-1