LCOV - code coverage report
Current view: top level - pkg/packagekit - updates.jsx Coverage Total Hit
Test: cockpit Lines: 67.8 % 1287 873
Test Date: 2026-06-16 14:09:37

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2017 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : import '../lib/patternfly/patternfly-6-cockpit.scss';
       6              : import 'polyfills'; // once per application
       7              : import 'cockpit-dark-theme'; // once per page
       8              : 
       9            1 : import cockpit from "cockpit";
      10            1 : import React from "react";
      11            1 : import { createRoot } from 'react-dom/client';
      12              : 
      13              : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
      14              : import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
      15              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      16              : import { CodeBlock, CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
      17              : import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
      18              : import {
      19              :     Modal, ModalBody, ModalFooter, ModalHeader
      20              : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      21              : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
      22              : import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
      23              : import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
      24              : import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
      25              : import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
      26              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      27              : import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
      28              : import { LabelGroup } from "@patternfly/react-core/dist/esm/components/Label/index.js";
      29              : import { Page, PageSection, } from "@patternfly/react-core/dist/esm/components/Page/index.js";
      30              : import { Progress, ProgressSize } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
      31              : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
      32              : import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
      33              : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
      34              : import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
      35              : 
      36              : import {
      37              :     BugIcon,
      38              :     CheckIcon,
      39              :     EnhancementIcon,
      40              :     ExclamationCircleIcon,
      41              :     ExclamationTriangleIcon,
      42              :     RebootingIcon,
      43              :     RedoIcon,
      44              :     ProcessAutomationIcon,
      45              :     SecurityIcon,
      46              : } from "@patternfly/react-icons";
      47              : import { TableText } from "@patternfly/react-table";
      48              : import { Remarkable } from "remarkable";
      49              : 
      50              : import { AutoUpdates, getBackend } from "./autoupdates.jsx";
      51              : import { KpatchSettings, KpatchStatus } from "./kpatch";
      52              : import { History, PackageList } from "./history";
      53              : import { page_status } from "notifications";
      54              : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
      55              : import { ListingTable } from 'cockpit-components-table.jsx';
      56              : import { ModalError } from 'cockpit-components-inline-notification.jsx';
      57              : import { ShutdownModal } from 'cockpit-components-shutdown.jsx';
      58              : import { WithDialogs } from "dialogs.jsx";
      59              : 
      60              : import { superuser } from 'superuser';
      61              : import * as PK from "packagekit.js";
      62              : import * as python from "python.js";
      63              : import * as timeformat from "timeformat";
      64              : 
      65              : import { debug, watchRedHatSubscription } from './utils';
      66              : import { read_os_release } from "os-release.js";
      67              : import callTracerScript from './callTracer.py';
      68              : 
      69              : import "./updates.scss";
      70              : import { Truncate } from '@patternfly/react-core/dist/esm/components/Truncate/index.js';
      71              : import { Severity } from '_internal/packagemanager-abstract';
      72              : import { getPackageManager } from 'packagemanager';
      73              : import { Icon } from '@patternfly/react-core/dist/esm/components/Icon/index.js';
      74              : 
      75            1 : const _ = cockpit.gettext;
      76              : 
      77              : // "available" heading is built dynamically
      78            1 : const STATE_HEADINGS = {};
      79            1 : const PK_STATUS_STRINGS = {};
      80            1 : const PK_STATUS_LOG_STRINGS = {};
      81              : 
      82            1 : const UPDATES = {
      83            1 :     ALL: 0,
      84            1 :     SECURITY: 1,
      85            1 :     KPATCHES: 2,
      86            1 : };
      87              : 
      88            1 : function init() {
      89            1 :     STATE_HEADINGS.loading = _("Loading available updates, please wait...");
      90            1 :     STATE_HEADINGS.locked = _("Some other program is currently using the package manager, please wait...");
      91            1 :     STATE_HEADINGS.refreshing = _("Refreshing package information");
      92            1 :     STATE_HEADINGS.uptodate = _("System is up to date");
      93            1 :     STATE_HEADINGS.applying = _("Applying updates");
      94            1 :     STATE_HEADINGS.updateError = _("Applying updates failed");
      95            1 :     STATE_HEADINGS.loadError = _("Loading available updates failed");
      96              : 
      97            1 :     PK_STATUS_STRINGS[PK.Enum.STATUS_DOWNLOAD] = _("Downloading");
      98            1 :     PK_STATUS_STRINGS[PK.Enum.STATUS_INSTALL] = _("Installing");
      99            1 :     PK_STATUS_STRINGS[PK.Enum.STATUS_UPDATE] = _("Updating");
     100            1 :     PK_STATUS_STRINGS[PK.Enum.STATUS_CLEANUP] = _("Setting up");
     101            1 :     PK_STATUS_STRINGS[PK.Enum.STATUS_SIGCHECK] = _("Verifying");
     102              : 
     103            1 :     PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_DOWNLOAD] = _("Downloaded");
     104            1 :     PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_INSTALL] = _("Installed");
     105            1 :     PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_UPDATE] = _("Updated");
     106            1 :     PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_CLEANUP] = _("Set up");
     107            1 :     PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_SIGCHECK] = _("Verified");
     108            1 : }
     109              : 
     110            1 : function deduplicate(list) {
     111            1 :     return [...new Set(list)].sort();
     112            1 : }
     113              : 
     114              : // Insert comma strings in between elements of the list. Unlike list.join(",")
     115              : // this does not stringify the elements, which we need to keep as JSX objects.
     116            0 : function insertCommas(list) {
     117            0 :     if (list.length <= 1)
     118            0 :         return list;
     119            0 :     return list.reduce((prev, cur) => [prev, ", ", cur]);
     120            0 : }
     121              : 
     122              : // Fedora changelogs are a wild mix of enumerations or not, headings, etc.
     123              : // Remove that formatting to avoid an untidy updates overview list
     124            1 : function cleanupChangelogLine(text) {
     125            1 :     if (!text)
     126            1 :         return text;
     127              : 
     128              :     // enumerations
     129            1 :     text = text.replace(/^[-* ]*/, "");
     130              : 
     131              :     // headings
     132            1 :     text = text.replace(/^=+\s+/, "").replace(/=+\s*$/, "");
     133              : 
     134            1 :     return text.trim();
     135            1 : }
     136              : 
     137              : // Replace cockpit-wsinstance-https@[long_id] with a shorter string
     138            1 : function shortenCockpitWsInstance(list) {
     139            0 :     return list.map(item => item.startsWith('cockpit-wsinstance-https') ? 'cockpit-wsinstance-https@.' : item);
     140            1 : }
     141              : 
     142            1 : function count_security_updates(updates) {
     143            1 :     let num_security = 0;
     144            1 :     for (const u of updates)
     145            1 :         if (u.severity === Severity.CRITICAL)
     146            1 :             ++num_security;
     147            1 :     return num_security;
     148            1 : }
     149              : 
     150            1 : function isKpatchPackage(name) {
     151            1 :     return name.startsWith("kpatch-patch");
     152            1 : }
     153              : 
     154            1 : function count_kpatch_updates(updates) {
     155            1 :     let num_kpatches = 0;
     156            1 :     for (const u of updates)
     157            1 :         if (isKpatchPackage(u.name))
     158            1 :             ++num_kpatches;
     159            1 :     return num_kpatches;
     160            1 : }
     161              : 
     162            1 : function find_highest_severity(updates) {
     163            1 :     let max = Severity.LOW;
     164            1 :     for (const u of updates)
     165            1 :         if (u.severity > max)
     166            1 :             max = u.severity;
     167            1 :     return max;
     168            1 : }
     169              : 
     170              : /**
     171              :  * Get appropriate icon for an update severity
     172              :  *
     173              :  * info: An Severity level
     174              :  * secSeverity: If given, further classification of the severity of Severity.CRITICAL from the vendor_urls;
     175              :  *              e. g. "critical", see https://access.redhat.com/security/updates/classification
     176              :  * Returns: Icon JSX object
     177              :  *
     178              :  */
     179            1 : function getSeverityIcon(info, secSeverity) {
     180            1 :     let classes = "severity-icon";
     181            1 :     if (secSeverity)
     182            1 :         classes += " severity-" + secSeverity;
     183            1 :     if (info == Severity.CRITICAL)
     184            1 :         return <Icon isInline status="danger"><SecurityIcon aria-label={secSeverity || _("security")} className={classes} /></Icon>;
     185            1 :     else if (info >= Severity.IMPORTANT)
     186            1 :         return <Icon isInline className='pf-m-important'><BugIcon className={classes} aria-label={_("bug fix")} /></Icon>;
     187              :     else
     188            1 :         return <Icon isInline status="custom"><EnhancementIcon className={classes} aria-label={_("enhancement")} /></Icon>;
     189            1 : }
     190              : 
     191            1 : function getPageStatusSeverityIcon(severity) {
     192            1 :     if (severity == Severity.CRITICAL)
     193            1 :         return "security";
     194            1 :     else if (severity >= Severity.IMPORTANT)
     195            1 :         return "bug";
     196              :     else
     197            1 :         return "enhancement";
     198            1 : }
     199              : 
     200            1 : function getSeverityURL(urls) {
     201            1 :     if (!urls)
     202            1 :         return null;
     203              : 
     204              :     // in ascending severity
     205            1 :     const knownLevels = ["low", "moderate", "important", "critical"];
     206            1 :     let highestIndex = -1;
     207            1 :     let highestURL = null;
     208              : 
     209              :     // search URLs for highest valid severity; by all means we expect an update to have at most one, but for paranoia..
     210            0 :     urls.forEach(value => {
     211            0 :         if (value.startsWith("https://access.redhat.com/security/updates/classification/#")) {
     212            0 :             const i = knownLevels.indexOf(value.slice(value.indexOf("#") + 1));
     213            0 :             if (i > highestIndex) {
     214            0 :                 highestIndex = i;
     215            0 :                 highestURL = value;
     216            0 :             }
     217            0 :         }
     218            0 :     });
     219            1 :     return highestURL;
     220            1 : }
     221              : 
     222              : // Overrides the link_open function to apply our required HTML attributes
     223            1 : function customRemarkable() {
     224            1 :     const remarkable = new Remarkable();
     225              : 
     226            1 :     const orig_link_open = remarkable.renderer.rules.link_open;
     227            0 :     remarkable.renderer.rules.link_open = function() {
     228            0 :         let result = orig_link_open.apply(null, arguments);
     229              : 
     230            0 :         const parser = new DOMParser();
     231            0 :         const htmlDocument = parser.parseFromString(result, "text/html");
     232            0 :         const links = htmlDocument.getElementsByTagName("a");
     233            0 :         if (links.length === 1) {
     234            0 :             const href = links[0].getAttribute("href");
     235            0 :             result = `<a rel="noopener noreferrer" target="_blank" href="${href}">`;
     236            0 :         }
     237            0 :         return result;
     238            0 :     };
     239            1 :     return remarkable;
     240            1 : }
     241              : 
     242            1 : function updateItem(remarkable, info, pkgNames, key) {
     243            1 :     let bugs = null;
     244            1 :     if (info.bug_urls && info.bug_urls.length) {
     245              :         // we assume a bug URL ends with a number; if not, show the complete URL
     246            0 :         bugs = insertCommas(info.bug_urls.map(url => (
     247            0 :             <a key={url} rel="noopener noreferrer" target="_blank" href={url}>
     248            0 :                 {url.match(/[0-9]+$/) || url}
     249            0 :             </a>)
     250            1 :         ));
     251            1 :     }
     252              : 
     253            1 :     let cves = null;
     254            1 :     if (info.cve_urls && info.cve_urls.length) {
     255            0 :         cves = insertCommas(info.cve_urls.map(url => (
     256            0 :             <a key={url} href={url} rel="noopener noreferrer" target="_blank">
     257            0 :                 {url.match(/[^/=]+$/)}
     258            0 :             </a>)
     259            1 :         ));
     260            1 :     }
     261              : 
     262            1 :     let errata = null;
     263            1 :     if (info.vendor_urls) {
     264            0 :         errata = insertCommas(info.vendor_urls.filter(url => url.indexOf("/errata/") > 0).map(url => (
     265            0 :             <a key={url} href={url} rel="noopener noreferrer" target="_blank">
     266            0 :                 {url.match(/[^/=]+$/)}
     267            0 :             </a>)
     268            1 :         ));
     269            1 :         if (!errata.length)
     270            1 :             errata = null; // simpler testing below
     271            1 :     }
     272              : 
     273            1 :     let secSeverityURL = getSeverityURL(info.vendor_urls);
     274            1 :     const secSeverity = secSeverityURL ? secSeverityURL.slice(secSeverityURL.indexOf("#") + 1) : null;
     275            1 :     const icon = getSeverityIcon(info.severity, secSeverity);
     276            1 :     let type;
     277            1 :     if (info.severity === Severity.CRITICAL) {
     278            1 :         if (secSeverityURL)
     279            1 :             secSeverityURL = <a rel="noopener noreferrer" target="_blank" href={secSeverityURL}>{secSeverity}</a>;
     280            1 :         type = (
     281            1 :             <Tooltip id="tip-severity" content={ secSeverity || _("security") }>
     282            1 :                 <span>
     283            1 :                     {icon}
     284            1 :                     { (info.cve_urls && info.cve_urls.length > 0) ? info.cve_urls.length : "" }
     285            1 :                 </span>
     286            1 :             </Tooltip>
     287              :         );
     288            1 :     } else {
     289            1 :         const tip = (info.severity >= Severity.IMPORTANT) ? _("bug fix") : _("enhancement");
     290            1 :         type = (
     291            1 :             <Tooltip id="tip-severity" content={tip}>
     292            1 :                 <span>
     293            1 :                     {icon}
     294            1 :                     { bugs ? info.bug_urls.length : "" }
     295            1 :                 </span>
     296            1 :             </Tooltip>
     297              :         );
     298            1 :     }
     299              : 
     300            1 :     const pkgList = pkgNames.map((n, index) => (
     301            1 :         <Tooltip key={n.name + n.arch} id="tip-summary" content={n.summary + " (" + n.arch + ")"}>
     302            1 :             <span>{n.name + (index !== (pkgNames.length - 1) ? ", " : "")}</span>
     303            1 :         </Tooltip>)
     304            1 :     );
     305            1 :     const pkgs = pkgList;
     306            1 :     const pkgsTruncated = pkgList.slice(0, 4);
     307              : 
     308            1 :     if (pkgList.length > 4)
     309            1 :         pkgsTruncated.push(<span key="more">…</span>);
     310              : 
     311            1 :     if (pkgNames.some(pkg => isKpatchPackage(pkg.name)))
     312            1 :         pkgsTruncated.push(
     313            1 :             <LabelGroup key={`${key}-kpatches-labelgroup`} className="kpatches-labelgroup">
     314            1 :                 {" "}<Badge color="blue">{_("patches")}</Badge>
     315            1 :             </LabelGroup>
     316            1 :         );
     317              : 
     318            1 :     let descriptionFirstLine = (info.description || "").trim();
     319            1 :     if (descriptionFirstLine.indexOf("\n") >= 0)
     320            1 :         descriptionFirstLine = descriptionFirstLine.slice(0, descriptionFirstLine.indexOf("\n"));
     321            1 :     descriptionFirstLine = cleanupChangelogLine(descriptionFirstLine);
     322            1 :     let description;
     323            1 :     if (info.markdown) {
     324            1 :         descriptionFirstLine = <span dangerouslySetInnerHTML={{ __html: remarkable.render(descriptionFirstLine) }} />;
     325            1 :         description = <div dangerouslySetInnerHTML={{ __html: remarkable.render(info.description) }} />;
     326            1 :     } else {
     327            1 :         description = <div className="changelog">{info.description}</div>;
     328            1 :     }
     329              : 
     330            1 :     const expandedContent = (
     331            1 :         <Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
     332            1 :             <DescriptionList>
     333            1 :                 <DescriptionListGroup>
     334            1 :                     <DescriptionListTerm>{_("Packages")}</DescriptionListTerm>
     335            1 :                     <DescriptionListDescription>{pkgs}</DescriptionListDescription>
     336            1 :                 </DescriptionListGroup>
     337            1 :                 { cves
     338            1 :                     ? <DescriptionListGroup>
     339            1 :                         <DescriptionListTerm>{_("CVE")}</DescriptionListTerm>
     340            1 :                         <DescriptionListDescription>{cves}</DescriptionListDescription>
     341            1 :                     </DescriptionListGroup>
     342            1 :                     : null }
     343            1 :                 { secSeverityURL
     344            1 :                     ? <DescriptionListGroup>
     345            1 :                         <DescriptionListTerm>{_("Severity")}</DescriptionListTerm>
     346            1 :                         <DescriptionListDescription className="severity">{secSeverityURL}</DescriptionListDescription>
     347            1 :                     </DescriptionListGroup>
     348            1 :                     : null }
     349            1 :                 { errata
     350            1 :                     ? <DescriptionListGroup>
     351            1 :                         <DescriptionListTerm>{_("Errata")}</DescriptionListTerm>
     352            1 :                         <DescriptionListDescription>{errata}</DescriptionListDescription>
     353            1 :                     </DescriptionListGroup>
     354            1 :                     : null }
     355            1 :                 { bugs
     356            1 :                     ? <DescriptionListGroup>
     357            1 :                         <DescriptionListTerm>{_("Bugs")}</DescriptionListTerm>
     358            1 :                         <DescriptionListDescription>{bugs}</DescriptionListDescription>
     359            1 :                     </DescriptionListGroup>
     360            1 :                     : null }
     361            1 :             </DescriptionList>
     362            1 :             <Content>{description}</Content>
     363            1 :         </Flex>
     364              :     );
     365              : 
     366            1 :     return {
     367            1 :         columns: [
     368            1 :             { title: pkgsTruncated },
     369            1 :             { title: <TableText wrapModifier="truncate">{info.version}</TableText>, props: { className: "version" } },
     370            1 :             { title: <TableText wrapModifier="nowrap">{type}</TableText>, props: { className: "type" } },
     371            1 :             { title: descriptionFirstLine, props: { className: "changelog" } },
     372            1 :         ],
     373            1 :         props: {
     374            1 :             key,
     375            1 :             className: info.severity === Severity.CRITICAL ? ["error"] : [],
     376            1 :         },
     377            1 :         hasPadding: true,
     378            1 :         expandedContent,
     379            1 :     };
     380            1 : }
     381              : 
     382            1 : const UpdatesList = ({ updates }) => {
     383            1 :     const remarkable = customRemarkable();
     384            1 :     const combined_updates = [];
     385              : 
     386              :     // PackageKit doesn"t expose source package names, so group packages with the same version and changelog
     387              :     // create a reverse version+changes → [id] map on iteration
     388            1 :     const sameUpdate = {};
     389            1 :     const packageNames = {};
     390            1 :     for (const u of updates) {
     391              :         // did we already see the same version and description? then merge
     392            1 :         const hash = u.version + u.description;
     393            1 :         const seenId = sameUpdate[hash];
     394            1 :         if (seenId) {
     395            1 :             packageNames[seenId].push({ name: u.name, arch: u.arch, summary: u.summary });
     396            1 :         } else {
     397              :             // this is a new update
     398            1 :             sameUpdate[hash] = u.id;
     399            1 :             packageNames[u.id] = [{ name: u.name, arch: u.arch, summary: u.summary }];
     400            1 :             combined_updates.push(u);
     401            1 :         }
     402            1 :     }
     403              : 
     404              :     // sort security first
     405            0 :     combined_updates.sort((a, b) => {
     406            0 :         if (a.severity === Severity.CRITICAL && b.severity !== Severity.CRITICAL)
     407            0 :             return -1;
     408            0 :         if (a.severity !== Severity.CRITICAL && b.severity === Severity.CRITICAL)
     409            0 :             return 1;
     410            0 :         return a.name.localeCompare(b.name);
     411            0 :     });
     412              : 
     413            1 :     return (
     414            1 :         <ListingTable aria-label={_("Available updates")}
     415            1 :                 gridBreakPoint='grid-lg'
     416            1 :                 columns={[
     417            1 :                     { title: _("Name"), props: { width: 40 } },
     418            1 :                     { title: _("Version"), props: { width: 15 } },
     419            1 :                     { title: _("Severity"), props: { width: 15 } },
     420            1 :                     { title: _("Details"), props: { width: 30 } },
     421            1 :                 ]}
     422            1 :                 rows={combined_updates.map(update => updateItem(remarkable, update, packageNames[update.id].sort((a, b) => a.name > b.name), update.id))} />
     423              :     );
     424            1 : };
     425              : 
     426            1 : class RestartServices extends React.Component {
     427            0 :     constructor(props) {
     428            0 :         super(props);
     429            0 :         this.state = {
     430            0 :             dialogError: undefined,
     431            0 :             restartInProgress: false,
     432            0 :         };
     433              : 
     434            0 :         this.dialogErrorSet = this.dialogErrorSet.bind(this);
     435            0 :         this.dialogErrorDismiss = this.dialogErrorDismiss.bind(this);
     436            0 :         this.restart = this.restart.bind(this);
     437            0 :     }
     438              : 
     439            0 :     dialogErrorSet(text, detail) {
     440            0 :         this.setState({ dialogError: text, dialogErrorDetail: detail });
     441            0 :     }
     442              : 
     443            0 :     dialogErrorDismiss() {
     444            0 :         this.setState({ dialogError: undefined });
     445            0 :     }
     446              : 
     447            0 :     restart() {
     448              :         // make sure cockpit package is the last to restart
     449            0 :         const daemons = this.props.restartPackages.daemons.sort((a, b) => {
     450            0 :             if (a.includes("cockpit") && b.includes("cockpit"))
     451            0 :                 return 0;
     452            0 :             if (a.includes("cockpit"))
     453            0 :                 return 1;
     454            0 :             return a.localeCompare(b);
     455            0 :         });
     456            0 :         const restarts = daemons.map(service => cockpit.spawn(["systemctl", "restart", service], { superuser: "require", err: "message" }));
     457            0 :         this.setState({ restartInProgress: true });
     458            0 :         Promise.all(restarts)
     459            0 :                 .then(() => {
     460            0 :                     this.props.onValueChanged({ restartPackages: { reboot: this.props.restartPackages.reboot, daemons: [], manual: this.props.restartPackages.manual } });
     461            0 :                     if (this.props.state === "updateSuccess")
     462            0 :                         this.props.loadUpdates();
     463            0 :                     this.setState({ restartInProgress: false });
     464            0 :                     this.props.close();
     465            0 :                 })
     466            0 :                 .catch(ex => {
     467            0 :                     this.dialogErrorSet(_("Failed to restart service"), ex.message);
     468              :                     // see what services remain
     469            0 :                     this.props.checkNeedsRestart();
     470            0 :                 });
     471            0 :     }
     472              : 
     473            0 :     render() {
     474            0 :         let body;
     475            0 :         if (this.props.checkRestartRunning) {
     476            0 :             body = (
     477            0 :                 <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
     478            0 :                     <Spinner size="sm" />
     479            0 :                     <p>{_("Reloading the state of remaining services")}</p>
     480            0 :                 </Flex>
     481              :             );
     482            0 :         } else if (this.props.restartPackages.daemons.length > 0) {
     483            0 :             body = (<>
     484            0 :                 {cockpit.ngettext("The following service will be restarted:", "The following services will be restarted:", this.props.restartPackages.daemons.length)}
     485            0 :                 <TwoColumnContent list={this.props.restartPackages.daemons} flexClassName="restart-services-modal-body" />
     486            0 :             </>);
     487            0 :         }
     488              : 
     489            0 :         return (
     490            0 :             <Modal id="restart-services-modal" isOpen
     491            0 :                    position="top"
     492            0 :                    variant="medium"
     493            0 :                    onClose={this.props.close}>
     494            0 :                 <ModalHeader title={_("Restart services")} />
     495            0 :                 <ModalBody>
     496            0 :                     <Stack hasGutter>
     497            0 :                         {this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />}
     498            0 :                         <StackItem>{body}</StackItem>
     499            0 :                     </Stack>
     500            0 :                 </ModalBody>
     501            0 :                 <ModalFooter>
     502            0 :                     {this.props.restartPackages.daemons.includes("cockpit") &&
     503            0 :                         <Alert variant="warning"
     504            0 :                             title={_("Web Console will restart")}
     505            0 :                             isInline>
     506            0 :                             <p>
     507            0 :                                 {_("When the Web Console is restarted, you will no longer see progress information. However, the update process will continue in the background. Reconnect to continue watching the update process.")}
     508            0 :                             </p>
     509            0 :                         </Alert>}
     510            0 :                     <Button variant='primary'
     511            0 :                         isDisabled={ this.state.restartInProgress }
     512            0 :                         onClick={ this.restart }>
     513            0 :                         {_("Restart services")}
     514            0 :                     </Button>
     515            0 :                     <Button variant='link' className='btn-cancel' onClick={ this.props.close }>
     516            0 :                         {_("Cancel")}
     517            0 :                     </Button>
     518            0 :                 </ModalFooter>
     519            0 :             </Modal>
     520              :         );
     521            0 :     }
     522            1 : }
     523              : 
     524            1 : const formatPackageId = packageId => {
     525            1 :     const pfields = packageId.split(";");
     526            1 :     return pfields[0] + " " + pfields[1] + " (" + pfields[2] + ")";
     527            1 : };
     528              : 
     529              : // actions is a chronological list of { status, packageId } events that happen during applying updates
     530              : // status: see PK_STATUS_* at https://github.com/PackageKit/PackageKit/blob/main/lib/packagekit-glib2/pk-enum.h
     531            1 : const ApplyUpdates = ({ transactionProps, actions, onCancel, rebootAfter, setRebootAfter }) => {
     532            1 :     const remain = transactionProps.RemainingTime
     533            1 :         ? timeformat.distanceToNow(new Date().valueOf() + transactionProps.RemainingTime * 1000)
     534            1 :         : null;
     535              : 
     536            1 :     let percentage = transactionProps.Percentage || 0;
     537              :     // PackageKit sets this to 101 initially
     538            1 :     if (percentage > 100)
     539            1 :         percentage = 0;
     540              : 
     541              :     // scroll update log to the bottom, if it already is (almost) at the bottom
     542            1 :     const log = document.getElementById("update-log");
     543            1 :     if (log) {
     544            1 :         if (log.scrollHeight - log.clientHeight <= log.scrollTop + 2)
     545            1 :             log.scrollTop = log.scrollHeight;
     546            1 :     }
     547              : 
     548            1 :     const cancelButton = transactionProps.AllowCancel
     549            1 :         ? <Button variant="secondary" onClick={onCancel} size="sm">{_("Cancel")}</Button>
     550            1 :         : null;
     551              : 
     552            1 :     if (actions.length === 0 && percentage === 0) {
     553            1 :         return <EmptyStatePanel title={ _("Initializing...") }
     554            1 :                                 headingLevel="h5"
     555            1 :                                 secondary={cancelButton}
     556            1 :                                 loading
     557            1 :         />;
     558            1 :     }
     559              : 
     560            1 :     const lastAction = actions[actions.length - 1];
     561              :     // when resuming an upgrade, we did not get any Package signal yet; fall back to LastPackage
     562            1 :     const curPackage = formatPackageId(lastAction?.packageId || transactionProps.LastPackage || "");
     563            1 :     return (
     564            1 :         <div className="progress-main-view">
     565            1 :             <Grid hasGutter>
     566            1 :                 <GridItem span={12}>
     567            1 :                     <div className="progress-description pf-v6-u-display-flex">
     568            1 :                         <Spinner size="md" isInline />
     569            1 :                         <strong>{PK_STATUS_STRINGS[lastAction?.status] || PK_STATUS_STRINGS[PK.Enum.STATUS_UPDATE]}</strong>
     570              :                         &nbsp;
     571            1 :                         <Truncate content={curPackage} />
     572            1 :                     </div>
     573            1 :                     <Progress title={remain}
     574            1 :                               aria-label={remain ? _("Time remaining: ") : _("Update progress")}
     575            1 :                               value={percentage}
     576            1 :                               size={ProgressSize.sm}
     577            1 :                               className="pf-v6-u-mb-xs" />
     578            1 :                 </GridItem>
     579              : 
     580            1 :                 <GridItem span={3}>{cancelButton}</GridItem>
     581              : 
     582            1 :                 <GridItem span={12}>
     583            1 :                     <Switch id="reboot-after" isChecked={rebootAfter}
     584            1 :                             label={ _("Reboot after completion") }
     585            1 :                             onChange={setRebootAfter} />
     586            1 :                 </GridItem>
     587              : 
     588            1 :                 <GridItem span={12} className="update-log">
     589            0 :                     <ExpandableSection toggleText={_("View update log")} onToggle={() => {
     590              :                         // always scroll down on expansion
     591            0 :                         const log = document.getElementById("update-log");
     592            0 :                         log.scrollTop = log.scrollHeight;
     593            0 :                     }}>
     594            1 :                         <div id="update-log" className="update-log-content">
     595            1 :                             <table>
     596            1 :                                 <tbody>
     597            0 :                                     { actions.slice(0, -1).map((action, i) => (
     598            0 :                                         <tr key={action.packageId + i}>
     599            0 :                                             <th>{PK_STATUS_LOG_STRINGS[action.status] || PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_UPDATE]}</th>
     600            0 :                                             <td>{formatPackageId(action.packageId)}</td>
     601            0 :                                         </tr>)) }
     602            1 :                                 </tbody>
     603            1 :                             </table>
     604            1 :                         </div>
     605            1 :                     </ExpandableSection>
     606            1 :                 </GridItem>
     607            1 :             </Grid>
     608            1 :         </div>
     609              :     );
     610            1 : };
     611              : 
     612            0 : const TwoColumnContent = ({ list, flexClassName }) => {
     613            0 :     const half = Math.round(list.length / 2);
     614            0 :     const col1 = list.slice(0, half);
     615            0 :     const col2 = list.slice(half);
     616            0 :     return (
     617            0 :         <Flex className={flexClassName}>
     618            0 :             <FlexItem flex={{ default: 'flex_1' }}>
     619            0 :                 <Content component="ul">
     620            0 :                     {col1.map(item => (<Content component="li" key={item}>{item}</Content>))}
     621            0 :                 </Content>
     622            0 :             </FlexItem>
     623            0 :             {col2.length > 0 && <FlexItem flex={{ default: 'flex_1' }}>
     624            0 :                 <Content component="ul">
     625            0 :                     {col2.map(item => (<Content component="li" key={item}>{item}</Content>))}
     626            0 :                 </Content>
     627            0 :             </FlexItem>}
     628            0 :         </Flex>
     629              :     );
     630            0 : };
     631              : 
     632            0 : const TwoColumnTitle = ({ icon, str }) => {
     633            0 :     return (<>
     634            0 :         {icon}
     635            0 :         <span className="update-success-table-title">
     636            0 :             {str}
     637            0 :         </span>
     638            0 :     </>);
     639            0 : };
     640              : 
     641            1 : const UpdateSuccess = ({ onIgnore, openServiceRestartDialog, openRebootDialog, restart, manual, reboot, checkRestartAvailable, history }) => {
     642            0 :     if (!checkRestartAvailable) {
     643              :         /* tracer is not available any more in RHEL 10; as a special case, if only kpatch and kernel were
     644              :          * updated, don't reboot (as that's their whole raison d'être) */
     645            0 :         const pkgs = Object.keys(history[0]?.packages ?? {});
     646            0 :         const only_kpatch = pkgs.filter(p => p.startsWith("kpatch")).length > 0 &&
     647            0 :                             pkgs.filter(p => !p.startsWith("kernel") && !p.startsWith("kpatch")).length == 0;
     648              : 
     649            0 :         const paragraph = only_kpatch ? null : _("Updated packages may require a reboot to take effect.");
     650            0 :         const actions = only_kpatch
     651            0 :             ? <Button id="ignore" variant="primary" onClick={onIgnore}>{_("Continue")}</Button>
     652            0 :             : <>
     653            0 :                 <Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button>
     654            0 :                 <Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
     655            0 :             </>;
     656              : 
     657            0 :         return (<>
     658            0 :             <EmptyStatePanel icon={RebootingIcon}
     659            0 :                              title={ _("Update was successful") }
     660            0 :                              headingLevel="h5"
     661            0 :                              paragraph={paragraph}
     662            0 :                              secondary={actions} />
     663            0 :             { history[0]?.packages &&
     664            0 :             <div className="flow-list-blank-slate">
     665            0 :                 <ExpandableSection toggleText={_("Package information")}>
     666            0 :                     <PackageList packages={history[0].packages} />
     667            0 :                 </ExpandableSection>
     668            0 :             </div>
     669              :             }
     670            0 :         </>);
     671            0 :     }
     672              : 
     673            1 :     const entries = [];
     674            0 :     if (reboot.length > 0) {
     675            0 :         entries.push({
     676            0 :             columns: [
     677            0 :                 {
     678            0 :                     title: <TwoColumnTitle icon={<RebootingIcon />}
     679            0 :                                            str={cockpit.format(cockpit.ngettext("$0 package needs a system reboot",
     680            0 :                                                                                 "$0 packages need a system reboot",
     681            0 :                                                                                 reboot.length),
     682            0 :                                                                reboot.length)} />
     683            0 :                 },
     684            0 :             ],
     685            0 :             props: { key: "reboot", id: "reboot-row" },
     686            0 :             hasPadding: true,
     687            0 :             expandedContent: <TwoColumnContent list={reboot} />,
     688            0 :         });
     689            0 :     }
     690              : 
     691            0 :     if (restart.length > 0) {
     692            0 :         entries.push({
     693            0 :             columns: [
     694            0 :                 {
     695            0 :                     title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
     696            0 :                                            str={cockpit.format(cockpit.ngettext("$0 service needs to be restarted",
     697            0 :                                                                                 "$0 services need to be restarted",
     698            0 :                                                                                 restart.length),
     699            0 :                                                                restart.length)} />
     700            0 :                 },
     701            0 :             ],
     702            0 :             props: { key: "service", id: "service-row" },
     703            0 :             hasPadding: true,
     704            0 :             expandedContent: <TwoColumnContent list={restart} />,
     705            0 :         });
     706            0 :     }
     707              : 
     708            0 :     if (manual.length > 0) {
     709            0 :         entries.push({
     710            0 :             columns: [
     711            0 :                 {
     712            0 :                     title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
     713            0 :                                            str={_("Some software needs to be restarted manually")} />
     714            0 :                 }
     715            0 :             ],
     716            0 :             props: { key: "manual", id: "manual-row" },
     717            0 :             hasPadding: true,
     718            0 :             expandedContent: <TwoColumnContent list={manual} />,
     719            0 :         });
     720            0 :     }
     721              : 
     722            1 :     const showReboot = reboot.length > 0 || manual.length > 0;
     723              : 
     724            1 :     return (<>
     725            1 :         <EmptyStatePanel title={ _("Update was successful") }
     726            1 :             headingLevel="h5"
     727            1 :             secondary={
     728            1 :                 <>
     729            0 :                     { entries.length > 0 && <ListingTable aria-label={_("Update Success Table")}
     730            0 :                         columns={[{ title: _("Info") }]}
     731            0 :                         showHeader={false}
     732            0 :                         className="updates-success-table"
     733            0 :                         rows={entries} /> }
     734            1 :                     <div className="update-success-actions">
     735            0 :                         { showReboot && <Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button> }
     736            0 :                         { restart.length > 0 && <Button id="choose-service" variant={showReboot ? "secondary" : "primary"} onClick={openServiceRestartDialog}>{_("Restart services...")}</Button> }
     737            1 :                         { reboot.length > 0 || restart.length > 0 || manual.length > 0
     738            0 :                             ? <Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
     739            1 :                             : <Button id="ignore" variant="primary" onClick={onIgnore}>{_("Continue")}</Button> }
     740            1 :                     </div>
     741            1 :                 </>
     742            1 :             } />
     743            1 :         { history[0]?.packages &&
     744            1 :         <div className="flow-list-blank-slate">
     745            1 :             <ExpandableSection toggleText={_("Package information")}>
     746            1 :                 <PackageList packages={history[0].packages} />
     747            1 :             </ExpandableSection>
     748            1 :         </div>
     749              :         }
     750            1 :     </>);
     751            1 : };
     752              : 
     753            1 : const UpdatesStatus = ({ updates, highestSeverity, timeSinceRefresh, restartPackages, onValueChanged }) => {
     754            1 :     const numUpdates = updates.length;
     755            1 :     const numSecurity = count_security_updates(updates);
     756            1 :     const numRestartServices = restartPackages.daemons.length;
     757            1 :     const numManualSoftware = restartPackages.manual.length;
     758            1 :     const numRebootPackages = restartPackages.reboot.length;
     759            1 :     let lastChecked;
     760              :     // PackageKit returns G_MAXUINT if the db was never checked.
     761            1 :     if (timeSinceRefresh !== null && timeSinceRefresh !== 2 ** 32 - 1)
     762            1 :         lastChecked = cockpit.format(_("Last checked: $0"), timeformat.distanceToNow(new Date().valueOf() - timeSinceRefresh * 1000));
     763              : 
     764            1 :     const notifications = [];
     765            1 :     if (numUpdates > 0) {
     766            1 :         if (numUpdates == numSecurity) {
     767            1 :             const stateStr = cockpit.ngettext("$0 security fix available", "$0 security fixes available", numSecurity);
     768            1 :             notifications.push({
     769            1 :                 id: "security-updates-available",
     770            1 :                 stateStr: cockpit.format(stateStr, numSecurity),
     771            1 :                 icon: getSeverityIcon(highestSeverity),
     772            1 :                 secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
     773            1 :             });
     774            1 :         } else {
     775            1 :             let stateStr = cockpit.ngettext("$0 update available", "$0 updates available", numUpdates);
     776            1 :             if (numSecurity > 0)
     777            1 :                 stateStr += cockpit.ngettext(", including $1 security fix", ", including $1 security fixes", numSecurity);
     778            1 :             notifications.push({
     779            1 :                 id: "updates-available",
     780            1 :                 stateStr: cockpit.format(stateStr, numUpdates, numSecurity),
     781            1 :                 icon: getSeverityIcon(highestSeverity),
     782            1 :                 secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
     783            1 :             });
     784            1 :         }
     785            1 :     } else if (!numRestartServices && !numRebootPackages && !numManualSoftware) {
     786            1 :         notifications.push({
     787            1 :             id: "system-up-to-date",
     788            1 :             stateStr: STATE_HEADINGS.uptodate,
     789            1 :             icon: <CheckIcon color="green" />,
     790            1 :             secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
     791            1 :         });
     792            1 :     }
     793              : 
     794            1 :     if (numRebootPackages > 0) {
     795            1 :         const stateStr = cockpit.ngettext("$0 package needs a system reboot", "$0 packages need a system reboot", numRebootPackages);
     796            1 :         notifications.push({
     797            1 :             id: "packages-need-reboot",
     798            1 :             stateStr: cockpit.format(stateStr, numRebootPackages),
     799            1 :             icon: <RebootingIcon />,
     800            0 :             secondary: <Button variant="danger" onClick={() => onValueChanged("showRebootSystemDialog", true)}>
     801            1 :                 {_("Reboot system...")}
     802            1 :             </Button>
     803            1 :         });
     804            1 :     }
     805              : 
     806            1 :     if (numRestartServices > 0) {
     807            1 :         const stateStr = cockpit.ngettext("$0 service needs to be restarted", "$0 services need to be restarted", numRestartServices);
     808            1 :         notifications.push({
     809            1 :             id: "services-need-restart",
     810            1 :             stateStr: cockpit.format(stateStr, numRestartServices),
     811            1 :             icon: <ProcessAutomationIcon />,
     812            0 :             secondary: <Button variant="primary" onClick={() => onValueChanged("showRestartServicesDialog", true)}>
     813            1 :                 {_("Restart services...")}
     814            1 :             </Button>
     815            1 :         });
     816            1 :     }
     817              : 
     818            1 :     if (numManualSoftware > 0) {
     819            1 :         notifications.push({
     820            1 :             id: "processes-need-restart",
     821            1 :             stateStr: _("Some software needs to be restarted manually"),
     822            1 :             icon: <ProcessAutomationIcon />,
     823            1 :             secondary: <Content component={ContentVariants.small}>{restartPackages.manual.join(", ")}</Content>
     824            1 :         });
     825            1 :     }
     826              : 
     827            1 :     return (<Stack hasGutter>
     828            1 :         { notifications.map(notification => (
     829            1 :             <StackItem key={notification.id}>
     830            1 :                 <Flex flexWrap={{ default: 'nowrap' }} id={notification.id}>
     831            1 :                     <FlexItem>
     832            1 :                         {notification.icon}
     833            1 :                     </FlexItem>
     834            1 :                     <FlexItem>
     835            1 :                         <Stack>
     836            1 :                             <StackItem>
     837            1 :                                 <Content component={ContentVariants.p}>{notification.stateStr}</Content>
     838            1 :                             </StackItem>
     839            1 :                             <StackItem>
     840            1 :                                 { notification.secondary }
     841            1 :                             </StackItem>
     842            1 :                         </Stack>
     843            1 :                     </FlexItem>
     844            1 :                 </Flex>
     845            1 :             </StackItem>
     846            1 :         ))}
     847            1 :     </Stack>);
     848            1 : };
     849              : 
     850            1 : class CardsPage extends React.Component {
     851            1 :     constructor() {
     852            1 :         super();
     853            1 :         this.state = {
     854            1 :             autoupdates_backend: undefined,
     855            1 :         };
     856            1 :     }
     857              : 
     858            1 :     componentDidMount() {
     859            1 :         getBackend(this.props.backend).then(b => { this.setState({ autoupdates_backend: b }) });
     860            1 :     }
     861              : 
     862            1 :     render() {
     863            1 :         const cardContents = [];
     864            1 :         let settingsContent = null;
     865            1 :         const statusContent = <Stack hasGutter>
     866            1 :             <UpdatesStatus key="updates-status"
     867            1 :                                 updates={this.props.updates}
     868            1 :                                 onValueChanged={this.props.onValueChanged}
     869            1 :                                 restartPackages={this.props.restartPackages}
     870            1 :                                 highestSeverity={this.props.highestSeverity}
     871            1 :                                 timeSinceRefresh={this.props.timeSinceRefresh} />
     872            1 :             <KpatchStatus />
     873            1 :         </Stack>;
     874              : 
     875            1 :         if (this.state.autoupdates_backend) {
     876            1 :             settingsContent = <Stack hasGutter>
     877            1 :                 <AutoUpdates privileged={this.props.privileged} packagekit_backend={this.props.backend} initial_backend={this.state.autoupdates_backend} />
     878            1 :                 {cockpit.info.os_release && cockpit.info.os_release?.ID === "rhel" &&
     879            1 :                 <KpatchSettings privileged={this.props.privileged} />
     880              :                 }
     881            1 :             </Stack>;
     882            1 :         }
     883              : 
     884            1 :         cardContents.push({
     885            1 :             id: "status",
     886            1 :             className: settingsContent !== null ? "ct-card-info" : "",
     887            1 :             title: _("Status"),
     888            1 :             actions: (<Tooltip content={_("Check for updates")}>
     889            1 :                 <Button icon={<RedoIcon />} variant="secondary" onClick={this.props.handleRefresh} />
     890            1 :             </Tooltip>),
     891            1 :             body: statusContent,
     892            1 :         });
     893              : 
     894            1 :         if (settingsContent !== null) {
     895            1 :             cardContents.push({
     896            1 :                 id: "settings",
     897            1 :                 className: "ct-card-info",
     898            1 :                 title: _("Settings"),
     899            1 :                 body: settingsContent,
     900            1 :             });
     901            1 :         }
     902              : 
     903            1 :         if (this.props.state === "available") { // automatic updates are not tracked by PackageKit, hide history when they are enabled
     904            1 :             cardContents.push({
     905            1 :                 id: "available-updates",
     906            1 :                 title: _("Available updates"),
     907            1 :                 actions: (<div className="pk-updates--header--actions">
     908            1 :                     {this.props.cockpitUpdate &&
     909            1 :                         <Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
     910            1 :                             <FlexItem>
     911            1 :                                 <ExclamationTriangleIcon className="ct-icon-exclamation-triangle cockpit-update-warning-icon" />
     912            1 :                                 <strong className="cockpit-update-warning-text">
     913            1 :                                     <span className="pf-screen-reader">{_("Danger alert:")}</span>
     914            1 :                                     {_("Web Console will restart")}
     915            1 :                                 </strong>
     916            1 :                             </FlexItem>
     917            1 :                             <FlexItem>
     918            1 :                                 <Popover aria-label="More information popover"
     919            1 :                                          bodyContent={_("When the Web Console is restarted, you will no longer see progress information. However, the update process will continue in the background. Reconnect to continue watching the update process.")}>
     920            1 :                                     <Button variant="link" isInline>{_("More info...")}</Button>
     921            1 :                                 </Popover>
     922            1 :                             </FlexItem>
     923            1 :                         </Flex>}
     924            1 :                     {this.props.applyKpatches}
     925            1 :                     {this.props.applySecurity}
     926            1 :                     {this.props.applyAll}
     927            1 :                 </div>),
     928            1 :                 containsList: true,
     929            1 :                 body: <UpdatesList updates={this.props.updates} />
     930            1 :             });
     931            1 :         }
     932              : 
     933            1 :         if ((!this.state.autoupdates_backend || !this.state.autoupdates_backend.enabled) && this.props.history.length > 0) { // automatic updates are not tracked by PackageKit, hide history when they are enabled
     934            1 :             cardContents.push({
     935            1 :                 id: "update-history",
     936            1 :                 title: _("Update history"),
     937            1 :                 containsList: true,
     938            1 :                 body: <History packagekit={this.props.history} />
     939            1 :             });
     940            1 :         }
     941              : 
     942            1 :         return cardContents.map(card => {
     943            1 :             return (
     944            1 :                 <Card key={card.id} className={card.className} id={card.id}>
     945            1 :                     <CardHeader actions={{ actions: card.actions }}>
     946            1 :                         <CardTitle component="h2">{card.title}</CardTitle>
     947            1 :                     </CardHeader>
     948            1 :                     <CardBody className={card.containsList ? "contains-list" : null}>
     949            1 :                         {card.body}
     950            1 :                     </CardBody>
     951            1 :                 </Card>
     952              :             );
     953            1 :         });
     954            1 :     }
     955            1 : }
     956              : 
     957            1 : class OsUpdates extends React.Component {
     958            1 :     constructor() {
     959            1 :         super();
     960            1 :         this.state = {
     961            1 :             state: "loading",
     962            1 :             errorMessages: [],
     963            1 :             updates: [],
     964            1 :             timeSinceRefresh: null,
     965            1 :             loadPercent: null,
     966            1 :             cockpitUpdate: false,
     967            1 :             haveOsRepo: null,
     968            1 :             applyTransaction: null,
     969            1 :             applyTransactionProps: {},
     970            1 :             applyActions: [],
     971            1 :             history: [],
     972            1 :             unregistered: false,
     973            1 :             privileged: false,
     974            1 :             autoUpdatesEnabled: undefined,
     975            1 :             restartPackages: { daemons: [], manual: [], reboot: [] },
     976            1 :             checkRestartAvailable: false,
     977            1 :             checkRestartRunning: false,
     978            1 :             showRestartServicesDialog: false,
     979            1 :             showRebootSystemDialog: false,
     980            1 :             backend: "",
     981            1 :             rebootAfterSuccess: false,
     982            1 :             packageManager: null,
     983            1 :         };
     984            1 :         this.handleLoadError = this.handleLoadError.bind(this);
     985            1 :         this.handleRefresh = this.handleRefresh.bind(this);
     986            1 :         this.loadUpdates = this.loadUpdates.bind(this);
     987            1 :         this.onValueChanged = this.onValueChanged.bind(this);
     988            1 :         this.checkNeedsRestart = this.checkNeedsRestart.bind(this);
     989            1 :     }
     990              : 
     991            0 :     onValueChanged(key, value) {
     992            0 :         this.setState({ [key]: value });
     993            0 :     }
     994              : 
     995            1 :     async componentDidMount() {
     996            1 :         this._mounted = true;
     997            1 :         this.checkNeedsRestart();
     998              : 
     999            1 :         superuser.addEventListener("changed", this.handleSuperUserChange);
    1000              : 
    1001              :         // HACK: force usage of PackageKit backend
    1002            1 :         const packageManager = await getPackageManager(true);
    1003            1 :         const backend = await packageManager.get_backend();
    1004            1 :         this.setState({ packageManager, backend });
    1005              : 
    1006              :         // check if there is an upgrade in progress already; if so, switch to "applying" state right away
    1007            1 :         PK.call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTransactionList", [])
    1008            1 :                 .then(([transactions]) => {
    1009            1 :                     if (!this._mounted)
    1010            1 :                         return;
    1011              : 
    1012            1 :                     const promises = transactions.map(transactionPath => PK.call(
    1013            1 :                         transactionPath, "org.freedesktop.DBus.Properties", "Get", [PK.transactionInterface, "Role"]));
    1014              : 
    1015            1 :                     Promise.all(promises)
    1016            1 :                             .then(roles => {
    1017              :                                 // any transaction with UPDATE_PACKAGES role?
    1018            1 :                                 for (let idx = 0; idx < roles.length; ++idx) {
    1019            1 :                                     if (roles[idx][0].v === PK.Enum.ROLE_UPDATE_PACKAGES) {
    1020            1 :                                         this.watchUpdates(transactions[idx]);
    1021            1 :                                         return;
    1022            1 :                                     }
    1023            1 :                                 }
    1024              : 
    1025              :                                 // no running updates found, proceed to showing available updates
    1026            1 :                                 this.initialLoadOrRefresh();
    1027            1 :                             })
    1028            0 :                             .catch(ex => {
    1029            0 :                                 console.warn("GetTransactionList: failed to read PackageKit transaction roles:", ex.message);
    1030              :                                 // be robust, try to continue with loading updates anyway
    1031            0 :                                 this.initialLoadOrRefresh();
    1032            0 :                             });
    1033            1 :                 })
    1034            1 :                 .catch(this.handleLoadError);
    1035            1 :     }
    1036              : 
    1037            0 :     componentWillUnmount() {
    1038            0 :         this._mounted = false;
    1039            0 :         superuser.removeEventListener("changed", this.handleSuperUserChange);
    1040            0 :     }
    1041              : 
    1042            1 :     handleSuperUserChange = () => {
    1043            1 :         this.setState({ privileged: superuser.allowed });
    1044              :         // get out of error state when switching from unprivileged to privileged
    1045            1 :         if (superuser.allowed && this.state.state.indexOf("Error") >= 0)
    1046            1 :             this.loadUpdates();
    1047            1 :     };
    1048              : 
    1049            1 :     checkNeedsRestart() {
    1050            1 :         this.setState({ checkRestartRunning: true });
    1051            1 :         return python.spawn(callTracerScript, undefined, { err: "message", superuser: "require" })
    1052            1 :                 .then(output => {
    1053            1 :                     debug("tracer succeeded, output:", output);
    1054            1 :                     const restartPackages = JSON.parse(output);
    1055              :                     // Filter out duplicates
    1056            1 :                     restartPackages.reboot = deduplicate(shortenCockpitWsInstance(restartPackages.reboot));
    1057            1 :                     restartPackages.daemons = deduplicate(shortenCockpitWsInstance(restartPackages.daemons));
    1058            1 :                     restartPackages.manual = deduplicate(shortenCockpitWsInstance(restartPackages.manual));
    1059            1 :                     debug("tracer parsed restartPackages:", JSON.stringify(restartPackages));
    1060            1 :                     this.setState({ checkRestartAvailable: true, checkRestartRunning: false, restartPackages });
    1061            1 :                 })
    1062            0 :                 .catch((exception, data) => {
    1063              :                     // tracer not installed or supported (like on Arch)? then fall back to dnf needs-restarting
    1064            0 :                     if (exception.message?.includes("ModuleNotFoundError") ||
    1065            0 :                         exception.message?.includes("UnsupportedDistribution")) {
    1066            0 :                         debug('tracer not installed:', JSON.stringify(exception), "trying dnf needs-restarting");
    1067            0 :                         return this.checkDnfNeedsRestarting();
    1068            0 :                     }
    1069              : 
    1070              :                     // log the error except for some common cases: polkit does not allow it
    1071            0 :                     if (exception.problem !== "access-denied" &&
    1072              :                         // or unprivileged session
    1073            0 :                         exception.problem !== "authentication-failed" &&
    1074              :                         // or the session goes away while checking
    1075            0 :                         exception.problem !== "terminated")
    1076            0 :                         console.error(`Tracer failed: "${JSON.stringify(exception)}", data: "${JSON.stringify(data)}"`);
    1077              :                     else
    1078            0 :                         debug('tracer failed for uninteresting reason:', JSON.stringify(exception));
    1079              : 
    1080              :                     // When tracer fails, act like it's not available (demand reboot after every update)
    1081            0 :                     this.setState({
    1082            0 :                         checkRestartAvailable: false,
    1083            0 :                         checkRestartRunning: false,
    1084            0 :                         restartPackages: { reboot: [], daemons: [], manual: [] },
    1085            0 :                     });
    1086            0 :                 });
    1087            1 :     }
    1088              : 
    1089            0 :     checkDnfNeedsRestarting() {
    1090            0 :         const restartPackages = { reboot: [], daemons: [], manual: [] };
    1091              : 
    1092              :         // needs-restarting has no machine-readable API: https://issues.redhat.com/browse/RHEL-56139
    1093              :         // dnf5 needs-restarting also has no machine-readable API: https://github.com/rpm-software-management/dnf5/issues/2341
    1094              :         // --exclude-services was added much later, so check that first
    1095            0 :         return cockpit.spawn(["dnf", "needs-restarting", "--exclude-services"], { err: "message", superuser: "require" })
    1096            0 :                 .then(outManual => {
    1097            0 :                     debug("dnf needs-restarting --exclude-services succeeded:", outManual);
    1098              :                     // format: "pid : argv", e.g. "1234 : mydaemon 3600"
    1099            0 :                     outManual.trim()
    1100            0 :                             .split("\n")
    1101              :                             // HACK: https://issues.redhat.com/browse/RHEL-84657
    1102            0 :                             .filter(line => line.match(/^\d+ : /))
    1103            0 :                             .forEach(line => !line || restartPackages.manual.push(line));
    1104              : 
    1105            0 :                     return Promise.allSettled([
    1106            0 :                         cockpit.spawn(["dnf", "needs-restarting", "--services"], { err: "message", superuser: "require" }),
    1107              :                         // we can't get stdout for a failing process, thus needs script
    1108            0 :                         cockpit.script("! dnf needs-restarting --reboothint", undefined, { err: "message", superuser: "require" }),
    1109            0 :                     ])
    1110            0 :                             .then(([serviceResult, rebootResult]) => {
    1111              :                                 // --services format: one unit name per line
    1112            0 :                                 if (serviceResult.status == 'fulfilled') {
    1113            0 :                                     debug("dnf needs-restarting --services succeeded:", serviceResult.value);
    1114            0 :                                     serviceResult.value.trim()
    1115            0 :                                             .split("\n")
    1116              :                                             // HACK: https://issues.redhat.com/browse/RHEL-84657
    1117            0 :                                             .filter(line => line.endsWith(".service"))
    1118            0 :                                             .forEach(line => restartPackages.daemons.push(line));
    1119            0 :                                 } else {
    1120            0 :                                     console.error("dnf needs-restarting --services failed:", JSON.stringify(serviceResult.reason));
    1121            0 :                                 }
    1122              : 
    1123              :                                 // --reboothint format: "  * kernel-rt" plus header/footer; exit nonzero iff reboot required, inverted above
    1124            0 :                                 if (rebootResult.status == 'fulfilled') {
    1125            0 :                                     debug("dnf needs-restarting --reboothint exited nonzero, wants reboot:", rebootResult.value);
    1126            0 :                                     rebootResult.value.split("\n").forEach(line => {
    1127            0 :                                         if (line.startsWith("  * "))
    1128            0 :                                             restartPackages.reboot.push(line.substring(4));
    1129            0 :                                     });
    1130            0 :                                 } else {
    1131            0 :                                     debug("dnf needs-restarting --reboothint exited zero, no reboot");
    1132            0 :                                 }
    1133              : 
    1134            0 :                                 debug("dnf needs-restarting parsed packages:", JSON.stringify(restartPackages));
    1135            0 :                                 this.setState({ checkRestartAvailable: true, checkRestartRunning: false, restartPackages });
    1136            0 :                             });
    1137            0 :                 })
    1138            0 :                 .catch(ex => {
    1139              :                     // log the error except for some common cases: no dnf
    1140            0 :                     if (ex.problem !== "not-found" &&
    1141              :                         // plugin does not support --exclude-services
    1142            0 :                         !ex.message.includes("usage:") &&
    1143              :                         // polkit does not allow it
    1144            0 :                         ex.problem !== "access-denied" &&
    1145              :                         // or unprivileged session
    1146            0 :                         ex.problem !== "authentication-failed" &&
    1147              :                         // or the session goes away while checking
    1148            0 :                         ex.problem !== "terminated")
    1149            0 :                         console.error("dnf needs-restarting failed:", ex.toString());
    1150              : 
    1151              :                     // act like it's not available (demand reboot after every update)
    1152            0 :                     this.setState({ checkRestartAvailable: false, checkRestartRunning: false, restartPackages });
    1153            0 :                 });
    1154            0 :     }
    1155              : 
    1156            0 :     handleLoadError(ex) {
    1157            0 :         console.warn("loading available updates failed:", JSON.stringify(ex));
    1158              : 
    1159            0 :         if (!this._mounted)
    1160            0 :             return;
    1161              : 
    1162            0 :         if (ex.problem === "not-found" || ex.name?.includes("DBus.Error.ServiceUnknown"))
    1163            0 :             ex = _("PackageKit is not installed");
    1164            0 :         this.state.errorMessages.push(ex.detail || ex.message || ex);
    1165            0 :         this.setState({ state: "loadError" });
    1166            0 :     }
    1167              : 
    1168            1 :     loadUpdates() {
    1169            1 :         let cockpitUpdate = false;
    1170              : 
    1171            1 :         this.setState({ state: "loading" });
    1172              : 
    1173              :         // check if there is an available version of coreutils; this is a heuristics for unregistered RHEL
    1174              :         // systems to see if they need a subscription to get "proper" OS updates
    1175            1 :         this.state.packageManager.is_available(["coreutils"])
    1176            1 :                 .then((have_coreutils) => this.setState({ haveOsRepo: have_coreutils }),
    1177            0 :                       ex => console.warn("Resolving coreutils failed:", JSON.stringify(ex)))
    1178            1 :                 .then(() => this.state.packageManager.get_updates(true, null).then(updates => {
    1179            1 :                     debug("GetUpdates result:", updates);
    1180            1 :                     if (updates.length) {
    1181            1 :                         for (const update of updates) {
    1182            1 :                             if (update.name === 'cockpit-ws') {
    1183            1 :                                 cockpitUpdate = true;
    1184              :                             // Arch Linux has no cockpit-ws package
    1185            1 :                             } else if (update.name === 'cockpit' && this.state.backend === "alpm") {
    1186            1 :                                 cockpitUpdate = true;
    1187            1 :                             }
    1188            1 :                         }
    1189            1 :                         this.setState({ updates, cockpitUpdate, state: "available" });
    1190            1 :                     } else {
    1191            1 :                         this.setState({ updates: [], state: "uptodate" });
    1192            1 :                     }
    1193            1 :                     this.loadHistory();
    1194            1 :                 }))
    1195            1 :                 .catch(this.handleLoadError);
    1196            1 :     }
    1197              : 
    1198            1 :     async loadHistory() {
    1199            1 :         try {
    1200            1 :             const history = await this.state.packageManager.get_history();
    1201            1 :             this.setState({ history });
    1202            1 :         } catch (exc) {
    1203            1 :             console.warn("Failed to load old transactions (history):", exc);
    1204            1 :         }
    1205            1 :     }
    1206              : 
    1207            0 :     initialLoadOrRefresh() {
    1208            0 :         watchRedHatSubscription(registered => this.setState({ unregistered: !registered }));
    1209              : 
    1210            0 :         cockpit.addEventListener("visibilitychange", () => {
    1211            0 :             if (!cockpit.hidden)
    1212            0 :                 this.loadOrRefresh(false);
    1213            0 :         });
    1214              : 
    1215            0 :         if (!cockpit.hidden)
    1216            0 :             this.loadOrRefresh(true);
    1217              :         else
    1218            0 :             this.loadUpdates();
    1219            0 :     }
    1220              : 
    1221            0 :     async loadOrRefresh(always_load) {
    1222            0 :         try {
    1223            0 :             const seconds = await this.state.packageManager.get_last_refresh_time();
    1224            0 :             this.setState({ timeSinceRefresh: seconds });
    1225              : 
    1226              :             // automatically trigger refresh for ≥ 1 day or if never refreshed
    1227            0 :             if (seconds >= 24 * 3600 || seconds < 0)
    1228            0 :                 this.handleRefresh();
    1229            0 :             else if (always_load)
    1230            0 :                 this.loadUpdates();
    1231            0 :         } catch (exc) {
    1232            0 :             this.handleLoadError(exc);
    1233            0 :         }
    1234            0 :     }
    1235              : 
    1236            1 :     watchUpdates(transactionPath) {
    1237            1 :         this.setState({ state: "applying", applyTransaction: transactionPath, applyTransactionProps: {}, applyActions: [] });
    1238              : 
    1239            1 :         return PK.watchTransaction(transactionPath,
    1240            1 :                                    {
    1241            0 :                                        ErrorCode: (code, details) => this.state.errorMessages.push(details),
    1242              : 
    1243            1 :                                        Finished: exit => {
    1244            1 :                                            this.setState({ applyTransaction: null, applyTransactionProps: {}, applyActions: [] });
    1245              : 
    1246            1 :                                            if (exit === PK.Enum.EXIT_SUCCESS) {
    1247            1 :                                                this.setState({ state: "loading", loadPercent: null });
    1248            1 :                                                this.loadHistory().then(() => {
    1249            1 :                                                    if (this.state.checkRestartAvailable) {
    1250            1 :                                                        this.checkNeedsRestart()
    1251            1 :                                                                .finally(() => this.setState({ state: "updateSuccess" }));
    1252            0 :                                                    } else {
    1253            0 :                                                        this.setState({ state: "updateSuccess", loadPercent: null });
    1254            0 :                                                    }
    1255            1 :                                                });
    1256            0 :                                            } else if (exit === PK.Enum.EXIT_CANCELLED) {
    1257            0 :                                                if (this.state.checkRestartAvailable) {
    1258            0 :                                                    this.setState({ state: "loading", loadPercent: null });
    1259            0 :                                                    this.checkNeedsRestart();
    1260            0 :                                                }
    1261            0 :                                                this.loadUpdates();
    1262            0 :                                            } else {
    1263              :                                                // normally we get FAILED here with ErrorCodes; handle unexpected errors to allow for some debugging
    1264            0 :                                                if (exit !== PK.Enum.EXIT_FAILED)
    1265            0 :                                                    this.state.errorMessages.push(cockpit.format(_("PackageKit reported error code $0"), exit));
    1266            0 :                                                this.setState({ state: "updateError" });
    1267            0 :                                            }
    1268            1 :                                        },
    1269              : 
    1270              :                                        // not working/being used in at least Fedora
    1271            0 :                                        RequireRestart: (type, packageId) => console.log("update RequireRestart", type, packageId),
    1272              : 
    1273            1 :                                        Package: (status, packageId) => this.setState(old =>
    1274            1 :                                            ({ applyActions: [...old.applyActions, { status, packageId }] })
    1275            1 :                                        ),
    1276            1 :                                    },
    1277              : 
    1278            1 :                                    notify => this.setState(old =>
    1279            1 :                                        ({ applyTransactionProps: { ...old.applyTransactionProps, ...notify } })
    1280            1 :                                    )
    1281            1 :         )
    1282            0 :                 .catch(ex => {
    1283            0 :                     this.state.errorMessages.push(ex);
    1284            0 :                     this.setState({ state: "updateError" });
    1285            0 :                 });
    1286            1 :     }
    1287              : 
    1288            0 :     applyUpdates(type) {
    1289            0 :         let updates = [...this.state.updates];
    1290            0 :         if (type === UPDATES.SECURITY)
    1291            0 :             updates = updates.filter(update => update.severity === Severity.CRITICAL);
    1292            0 :         if (type === UPDATES.KPATCHES) {
    1293            0 :             updates = updates.filter(update => isKpatchPackage(update.name));
    1294            0 :         }
    1295              : 
    1296            0 :         PK.transaction()
    1297            0 :                 .then(transactionPath => {
    1298            0 :                     this.watchUpdates(transactionPath)
    1299            0 :                             .then(() => {
    1300            0 :                                 PK.update_packages(updates, null, transactionPath)
    1301            0 :                                         .catch(ex => {
    1302              :                                             // We get more useful error messages through ErrorCode or "PackageKit has crashed", so only
    1303              :                                             // show this if we don't have anything else
    1304            0 :                                             if (this.state.errorMessages.length === 0)
    1305            0 :                                                 this.state.errorMessages.push(ex.message);
    1306            0 :                                             this.setState({ state: "updateError" });
    1307            0 :                                         });
    1308            0 :                             });
    1309            0 :                 })
    1310            0 :                 .catch(ex => {
    1311            0 :                     this.state.errorMessages.push(ex.message);
    1312            0 :                     this.setState({ state: "updateError" });
    1313            0 :                 });
    1314            0 :     }
    1315              : 
    1316            1 :     renderContent() {
    1317            1 :         let applySecurity;
    1318            1 :         let applyKpatches;
    1319            1 :         let applyAll;
    1320              : 
    1321              :         /* On unregistered RHEL systems we need some heuristics: If the "main" OS repos (which provide coreutils) require
    1322              :          * a subscription, then point this out and don't show available updates, even if there are some auxiliary
    1323              :          * repositories enabled which don't require subscriptions. But there are a lot of cases (cloud repos, nightly internal
    1324              :          * repos) which don't need a subscription, there it would just be confusing */
    1325            1 :         if (this.state.unregistered && this.state.haveOsRepo === false) {
    1326            1 :             page_status.set_own({
    1327            1 :                 type: "warning",
    1328            1 :                 title: _("Not registered"),
    1329            1 :                 details: {
    1330            1 :                     link: "subscriptions",
    1331            1 :                 }
    1332            1 :             });
    1333              : 
    1334            1 :             return <EmptyStatePanel
    1335            1 :                 title={_("This system is not registered")}
    1336            1 :                 headingLevel="h5"
    1337            1 :                 paragraph={ _("To get software updates, this system needs to be registered with Red Hat, either using the Red Hat Customer Portal or a local subscription server.") }
    1338            1 :                 icon={ExclamationCircleIcon}
    1339            1 :                 action={ _("Register…") }
    1340            0 :                 onAction={ () => cockpit.jump("/subscriptions", cockpit.transport.host) }
    1341            1 :             />;
    1342            1 :         }
    1343              : 
    1344            1 :         switch (this.state.state) {
    1345            1 :         case "loading":
    1346            1 :         case "refreshing":
    1347            1 :         case "locked":
    1348            1 :             page_status.set_own({
    1349            1 :                 type: null,
    1350            1 :                 title: _("Checking for package updates..."),
    1351            1 :                 details: {
    1352            1 :                     link: false,
    1353            1 :                     pficon: "spinner",
    1354            1 :                 }
    1355            1 :             });
    1356              : 
    1357            1 :             if (this.state.loadPercent)
    1358            1 :                 return <Progress value={this.state.loadPercent} title={STATE_HEADINGS[this.state.state]} />;
    1359              :             else
    1360            1 :                 return <EmptyStatePanel loading title={ _("Checking software status")}
    1361            1 :                                         headingLevel="h5"
    1362            1 :                                         paragraph={STATE_HEADINGS[this.state.state]}
    1363            1 :                 />;
    1364              : 
    1365            1 :         case "available":
    1366            1 :         {
    1367            1 :             const num_updates = this.state.updates.length;
    1368            1 :             const num_security_updates = count_security_updates(this.state.updates);
    1369            1 :             const num_kpatches = count_kpatch_updates(this.state.updates);
    1370            1 :             const highest_severity = find_highest_severity(this.state.updates);
    1371              : 
    1372            1 :             applyAll = (
    1373            0 :                 <Button id={num_updates == num_security_updates ? "install-security" : "install-all"} variant="primary" onClick={ () => this.applyUpdates(UPDATES.ALL) }>
    1374            1 :                     { num_updates == num_security_updates
    1375            1 :                         ? _("Install security updates")
    1376            1 :                         : _("Install all updates") }
    1377            1 :                 </Button>);
    1378              : 
    1379            1 :             if (num_security_updates > 0 && num_updates > num_security_updates) {
    1380            1 :                 applySecurity = (
    1381            0 :                     <Button id="install-security" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.SECURITY) }>
    1382            1 :                         {_("Install security updates")}
    1383            1 :                     </Button>);
    1384            1 :             }
    1385              : 
    1386            1 :             if (num_kpatches > 0) {
    1387            1 :                 applyKpatches = (
    1388            0 :                     <Button id="install-kpatches" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.KPATCHES) }>
    1389            1 :                         {_("Install kpatch updates")}
    1390            1 :                     </Button>);
    1391            1 :             }
    1392              : 
    1393            1 :             let text;
    1394            1 :             if (highest_severity == Severity.CRITICAL)
    1395            1 :                 text = _("Security updates available");
    1396            1 :             else if (highest_severity >= Severity.IMPORTANT)
    1397            1 :                 text = _("Bug fix updates available");
    1398            1 :             else if (highest_severity >= Severity.LOW)
    1399            1 :                 text = _("Enhancement updates available");
    1400              :             else
    1401            1 :                 text = _("Updates available");
    1402              : 
    1403            1 :             page_status.set_own({
    1404            1 :                 type: num_security_updates > 0 ? "warning" : "info",
    1405            1 :                 title: text,
    1406            1 :                 details: {
    1407            1 :                     pficon: getPageStatusSeverityIcon(highest_severity)
    1408            1 :                 }
    1409            1 :             });
    1410              : 
    1411            1 :             return (
    1412            1 :                 <>
    1413            1 :                     <PageSection hasBodyWrapper={false}>
    1414            1 :                         <Gallery className='ct-cards-grid' hasGutter>
    1415            1 :                             <CardsPage handleRefresh={this.handleRefresh}
    1416            1 :                                        applySecurity={applySecurity}
    1417            1 :                                        applyAll={applyAll}
    1418            1 :                                        applyKpatches={applyKpatches}
    1419            1 :                                        highestSeverity={highest_severity}
    1420            1 :                                        onValueChanged={this.onValueChanged}
    1421            1 :                                        {...this.state} />
    1422            1 :                         </Gallery>
    1423            1 :                     </PageSection>
    1424            1 :                     { this.state.showRestartServicesDialog &&
    1425            1 :                         <RestartServices
    1426            1 :                             restartPackages={this.state.restartPackages}
    1427            0 :                             close={() => this.setState({ showRestartServicesDialog: false })}
    1428            1 :                             state={this.state.state}
    1429            1 :                             checkNeedsRestart={this.checkNeedsRestart}
    1430            0 :                             onValueChanged={delta => this.setState(delta)}
    1431            1 :                             loadUpdates={this.loadUpdates} />
    1432              :                     }
    1433            1 :                     { this.state.showRebootSystemDialog &&
    1434            0 :                         <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
    1435              :                     }
    1436            1 :                 </>
    1437              :             );
    1438            1 :         }
    1439              : 
    1440            1 :         case "loadError":
    1441            1 :         case "updateError":
    1442            1 :             page_status.set_own({
    1443            1 :                 type: "error",
    1444            1 :                 title: STATE_HEADINGS[this.state.state],
    1445            1 :             });
    1446            1 :             return (
    1447            1 :                 <Stack>
    1448            1 :                     <EmptyStatePanel title={ STATE_HEADINGS[this.state.state] }
    1449            1 :                                     icon={ ExclamationCircleIcon }
    1450            1 :                                     paragraph={
    1451            1 :                                         <Content component={ContentVariants.p}>
    1452            1 :                                             {_("Please resolve the issue and reload this page.")}
    1453            1 :                                         </Content>
    1454              :                                     }
    1455            1 :                     />
    1456            1 :                     <CodeBlock className='pf-v6-u-mx-auto error-log'>
    1457            1 :                         <CodeBlockCode>
    1458            1 :                             {this.state.errorMessages
    1459            0 :                                     .filter((m, index) => index == 0 || m != this.state.errorMessages[index - 1])
    1460            0 :                                     .map(m => <span key={m}>{m}</span>)}
    1461            1 :                         </CodeBlockCode>
    1462            1 :                     </CodeBlock>
    1463            1 :                 </Stack>
    1464              :             );
    1465              : 
    1466            1 :         case "applying":
    1467            1 :             page_status.set_own(null);
    1468            1 :             return <ApplyUpdates transactionProps={this.state.applyTransactionProps}
    1469            1 :                                  actions={this.state.applyActions}
    1470            0 :                                  onCancel={ () => PK.call(this.state.applyTransaction, PK.transactionInterface, "Cancel", []) }
    1471            1 :                                  rebootAfter={this.state.rebootAfterSuccess}
    1472            0 :                                  setRebootAfter={ (_event, enabled) => this.setState({ rebootAfterSuccess: enabled }) }
    1473            1 :             />;
    1474              : 
    1475            1 :         case "updateSuccess": {
    1476            1 :             if (this.state.rebootAfterSuccess) {
    1477            1 :                 this.setState({ state: "restart" });
    1478            1 :                 cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require" });
    1479            1 :                 return null;
    1480            1 :             }
    1481              : 
    1482            1 :             let warningTitle;
    1483            1 :             if (!this.state.checkRestartAvailable) {
    1484            1 :                 warningTitle = _("Reboot recommended");
    1485            1 :             } else {
    1486            1 :                 if (this.state.restartPackages.reboot.length > 0)
    1487            1 :                     warningTitle = cockpit.ngettext("A package needs a system reboot for the updates to take effect:",
    1488            1 :                                                     "Some packages need a system reboot for the updates to take effect:",
    1489            1 :                                                     this.state.restartPackages.reboot.length);
    1490            1 :                 else if (this.state.restartPackages.daemons.length > 0)
    1491            1 :                     warningTitle = cockpit.ngettext("A service needs to be restarted for the updates to take effect:",
    1492            1 :                                                     "Some services need to be restarted for the updates to take effect:",
    1493            1 :                                                     this.state.restartPackages.daemons.length);
    1494            1 :                 else if (this.state.restartPackages.manual.length > 0)
    1495            1 :                     warningTitle = _("Some software needs to be restarted manually");
    1496            1 :             }
    1497              : 
    1498            1 :             if (warningTitle) {
    1499            1 :                 page_status.set_own({
    1500            1 :                     type: "warning",
    1501            1 :                     title: warningTitle
    1502            1 :                 });
    1503            1 :             }
    1504              : 
    1505            1 :             return (
    1506            1 :                 <>
    1507            1 :                     <UpdateSuccess onIgnore={this.loadUpdates}
    1508            0 :                         openServiceRestartDialog={() => this.setState({ showRestartServicesDialog: true })}
    1509            0 :                         openRebootDialog={() => this.setState({ showRebootSystemDialog: true })}
    1510            1 :                         restart={this.state.restartPackages.daemons}
    1511            1 :                         manual={this.state.restartPackages.manual}
    1512            1 :                         reboot={this.state.restartPackages.reboot}
    1513            1 :                         checkRestartAvailable={this.state.checkRestartAvailable}
    1514            1 :                         history={this.state.history} />
    1515            1 :                     { this.state.showRebootSystemDialog &&
    1516            0 :                         <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
    1517              :                     }
    1518            1 :                     { this.state.showRestartServicesDialog &&
    1519            1 :                         <RestartServices restartPackages={this.state.restartPackages}
    1520            0 :                             close={() => this.setState({ showRestartServicesDialog: false })}
    1521            1 :                             state={this.state.state}
    1522            1 :                             checkNeedsRestart={this.checkNeedsRestart}
    1523            0 :                             onValueChanged={delta => this.setState(delta)}
    1524            1 :                             loadUpdates={this.loadUpdates} />
    1525              :                     }
    1526            1 :                 </>
    1527              :             );
    1528            1 :         }
    1529              : 
    1530            1 :         case "restart":
    1531            1 :             page_status.set_own(null);
    1532            1 :             return <EmptyStatePanel loading title={ _("Restarting") }
    1533            1 :                                     headingLevel="h5"
    1534            1 :                                     paragraph={ _("Your server will close the connection soon. You can reconnect after it has restarted.") }
    1535            1 :             />;
    1536              : 
    1537            1 :         case "uptodate":
    1538            1 :         {
    1539            1 :             page_status.set_own({
    1540            1 :                 title: STATE_HEADINGS[this.state.state],
    1541            1 :                 details: {
    1542            1 :                     link: false,
    1543            1 :                     pficon: "check",
    1544            1 :                 }
    1545            1 :             });
    1546              : 
    1547            1 :             return (
    1548            1 :                 <PageSection hasBodyWrapper={false}>
    1549            1 :                     <Gallery className='ct-cards-grid' hasGutter>
    1550            1 :                         <CardsPage onValueChanged={this.onValueChanged} handleRefresh={this.handleRefresh} {...this.state} />
    1551            1 :                     </Gallery>
    1552            1 :                     { this.state.showRestartServicesDialog &&
    1553            1 :                     <RestartServices restartPackages={this.state.restartPackages}
    1554            0 :                                      close={() => this.setState({ showRestartServicesDialog: false })}
    1555            1 :                                      state={this.state.state}
    1556            1 :                                      checkNeedsRestart={this.checkNeedsRestart}
    1557            0 :                                      onValueChanged={delta => this.setState(delta)}
    1558            1 :                                      loadUpdates={this.loadUpdates} />
    1559              :                     }
    1560            1 :                     { this.state.showRebootSystemDialog &&
    1561            0 :                     <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
    1562              :                     }
    1563            1 :                 </PageSection>
    1564              :             );
    1565            1 :         }
    1566              : 
    1567            1 :         default:
    1568            1 :             page_status.set_own(null);
    1569            1 :             return null;
    1570            1 :         }
    1571            1 :     }
    1572              : 
    1573            1 :     handleRefresh() {
    1574            1 :         this.setState({ state: "refreshing", loadPercent: null });
    1575            1 :         this.state.packageManager.refresh(true, data => this.setState({ loadPercent: data.percentage }))
    1576            1 :                 .then(() => {
    1577            1 :                     if (this._mounted === false)
    1578            1 :                         return;
    1579              : 
    1580            1 :                     this.setState({ timeSinceRefresh: 0 });
    1581            1 :                     this.loadUpdates();
    1582            1 :                 })
    1583            1 :                 .catch(this.handleLoadError);
    1584            1 :     }
    1585              : 
    1586            1 :     render() {
    1587            1 :         let content = this.renderContent();
    1588            1 :         if (!["available", "uptodate"].includes(this.state.state))
    1589            1 :             content = <PageSection hasBodyWrapper={false}>{content}</PageSection>;
    1590              : 
    1591            1 :         return (
    1592            1 :             <WithDialogs>
    1593            1 :                 <Page className="pf-m-no-sidebar">
    1594            1 :                     {content}
    1595            1 :                 </Page>
    1596            1 :             </WithDialogs>
    1597              :         );
    1598            1 :     }
    1599            1 : }
    1600              : 
    1601            1 : document.addEventListener("DOMContentLoaded", async () => {
    1602            1 :     init();
    1603              : 
    1604            1 :     try {
    1605            1 :         await cockpit.init();
    1606            1 :     } catch (exp) {
    1607              :         /* Remove this when every beiboot scenario has Cockpit 336 */
    1608            1 :         if (exp.problem === 'not-supported') {
    1609            1 :             const os_release = await read_os_release();
    1610            1 :             cockpit.info.os_release = os_release;
    1611            1 :         }
    1612            1 :     }
    1613              : 
    1614            1 :     const root = createRoot(document.getElementById('app'));
    1615            1 :     root.render(<OsUpdates />);
    1616            1 : });
        

Generated by: LCOV version 2.0-1