LCOV - code coverage report
Current view: top level - pkg/packagekit - updates.jsx Coverage Total Hit
Test: cockpit Lines: 90.2 % 1287 1161
Test Date: 2026-06-25 11:17:56

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

Generated by: LCOV version 2.0-1