LCOV - code coverage report
Current view: top level - pkg/packagekit - updates.jsx Coverage Total Hit
Test: cockpit Lines: 90.4 % 1278 1155
Test Date: 2026-07-17 12:03:54

            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           12 :         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           19 : function count_security_updates(updates) {
     143           19 :     let num_security = 0;
     144           19 :     for (const u of updates)
     145           16 :         if (u.severity === Severity.CRITICAL)
     146            5 :             ++num_security;
     147           19 :     return num_security;
     148           19 : }
     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           12 :         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           12 :     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            2 :     } else {
     327           12 :         description = <div className="changelog">{info.description}</div>;
     328           12 :     }
     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 :             dialogErrorDetail: undefined,
     432            2 :             restartInProgress: false,
     433            2 :         };
     434              : 
     435            2 :         this.restart = this.restart.bind(this);
     436            2 :     }
     437              : 
     438            2 :     restart() {
     439              :         // make sure cockpit package is the last to restart
     440            0 :         const daemons = this.props.restartPackages.daemons.sort((a, b) => {
     441            0 :             if (a.includes("cockpit") && b.includes("cockpit"))
     442            0 :                 return 0;
     443            0 :             if (a.includes("cockpit"))
     444            0 :                 return 1;
     445            0 :             return a.localeCompare(b);
     446            0 :         });
     447            2 :         const restarts = daemons.map(service => cockpit.spawn(["systemctl", "restart", service], { superuser: "require", err: "message" }));
     448            2 :         this.setState({ restartInProgress: true, dialogError: undefined, dialogErrorDetail: undefined });
     449            2 :         Promise.all(restarts)
     450            1 :                 .then(() => {
     451            1 :                     this.props.onValueChanged({ restartPackages: { reboot: this.props.restartPackages.reboot, daemons: [], manual: this.props.restartPackages.manual } });
     452            1 :                     if (this.props.state === "updateSuccess")
     453            1 :                         this.props.loadUpdates();
     454            1 :                     this.setState({ restartInProgress: false });
     455            1 :                     this.props.close();
     456            1 :                 })
     457            1 :                 .catch(ex => {
     458            1 :                     this.setState({ dialogError: _("Failed to restart service"), dialogErrorDetail: ex.message });
     459              :                     // see what services remain
     460            1 :                     this.props.checkNeedsRestart();
     461            1 :                 });
     462            2 :     }
     463              : 
     464            2 :     render() {
     465            2 :         let body;
     466            1 :         if (this.props.checkRestartRunning) {
     467            1 :             body = (
     468            1 :                 <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
     469            1 :                     <Spinner size="sm" />
     470            1 :                     <p>{_("Reloading the state of remaining services")}</p>
     471            1 :                 </Flex>
     472              :             );
     473            1 :         } else if (this.props.restartPackages.daemons.length > 0) {
     474            2 :             body = (<>
     475            2 :                 {cockpit.ngettext("The following service will be restarted:", "The following services will be restarted:", this.props.restartPackages.daemons.length)}
     476            2 :                 <TwoColumnContent list={this.props.restartPackages.daemons} flexClassName="restart-services-modal-body" />
     477            2 :             </>);
     478            2 :         }
     479              : 
     480            2 :         return (
     481            2 :             <Modal id="restart-services-modal" isOpen
     482            2 :                    position="top"
     483            2 :                    variant="medium"
     484            2 :                    onClose={this.props.close}>
     485            2 :                 <ModalHeader title={_("Restart services")} />
     486            2 :                 <ModalBody>
     487            2 :                     <Stack hasGutter>
     488            2 :                         {this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />}
     489            2 :                         <StackItem>{body}</StackItem>
     490            2 :                     </Stack>
     491            2 :                 </ModalBody>
     492            2 :                 <ModalFooter>
     493            2 :                     {this.props.restartPackages.daemons.includes("cockpit") &&
     494            1 :                         <Alert variant="warning"
     495            1 :                             title={_("Web Console will restart")}
     496            1 :                             isInline>
     497            1 :                             <p>
     498            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.")}
     499            1 :                             </p>
     500            1 :                         </Alert>}
     501            2 :                     <Button variant='primary'
     502            2 :                         isDisabled={ this.state.restartInProgress }
     503            2 :                         onClick={ this.restart }>
     504            2 :                         {_("Restart services")}
     505            2 :                     </Button>
     506            2 :                     <Button variant='link' className='btn-cancel' onClick={ this.props.close }>
     507            2 :                         {_("Cancel")}
     508            2 :                     </Button>
     509            2 :                 </ModalFooter>
     510            2 :             </Modal>
     511              :         );
     512            2 :     }
     513           19 : }
     514              : 
     515           11 : const formatPackageId = packageId => {
     516           11 :     const pfields = packageId.split(";");
     517           11 :     return pfields[0] + " " + pfields[1] + " (" + pfields[2] + ")";
     518           11 : };
     519              : 
     520              : // actions is a chronological list of { status, packageId } events that happen during applying updates
     521              : // status: see PK_STATUS_* at https://github.com/PackageKit/PackageKit/blob/main/lib/packagekit-glib2/pk-enum.h
     522           12 : const ApplyUpdates = ({ transactionProps, actions, onCancel, rebootAfter, setRebootAfter }) => {
     523           12 :     const remain = transactionProps.RemainingTime
     524            2 :         ? timeformat.distanceToNow(new Date().valueOf() + transactionProps.RemainingTime * 1000)
     525           12 :         : null;
     526              : 
     527           12 :     let percentage = transactionProps.Percentage || 0;
     528              :     // PackageKit sets this to 101 initially
     529           12 :     if (percentage > 100)
     530           12 :         percentage = 0;
     531              : 
     532              :     // scroll update log to the bottom, if it already is (almost) at the bottom
     533           12 :     const log = document.getElementById("update-log");
     534           11 :     if (log) {
     535           11 :         if (log.scrollHeight - log.clientHeight <= log.scrollTop + 2)
     536           11 :             log.scrollTop = log.scrollHeight;
     537           11 :     }
     538              : 
     539           12 :     const cancelButton = transactionProps.AllowCancel
     540           12 :         ? <Button variant="secondary" onClick={onCancel} size="sm">{_("Cancel")}</Button>
     541           12 :         : null;
     542              : 
     543           12 :     if (actions.length === 0 && percentage === 0) {
     544           12 :         return <EmptyStatePanel title={ _("Initializing...") }
     545           12 :                                 headingLevel="h5"
     546           12 :                                 secondary={cancelButton}
     547           12 :                                 loading
     548           12 :         />;
     549           12 :     }
     550              : 
     551           11 :     const lastAction = actions[actions.length - 1];
     552              :     // when resuming an upgrade, we did not get any Package signal yet; fall back to LastPackage
     553           10 :     const curPackage = formatPackageId(lastAction?.packageId || transactionProps.LastPackage || "");
     554           12 :     return (
     555           12 :         <div className="progress-main-view">
     556           12 :             <Grid hasGutter>
     557           12 :                 <GridItem span={12}>
     558           12 :                     <div className="progress-description pf-v6-u-display-flex">
     559           12 :                         <Spinner size="md" isInline />
     560           10 :                         <strong>{PK_STATUS_STRINGS[lastAction?.status] || PK_STATUS_STRINGS[PK.Enum.STATUS_UPDATE]}</strong>
     561              :                         &nbsp;
     562           12 :                         <Truncate content={curPackage} />
     563           12 :                     </div>
     564           12 :                     <Progress title={remain}
     565            2 :                               aria-label={remain ? _("Time remaining: ") : _("Update progress")}
     566           12 :                               value={percentage}
     567           12 :                               size={ProgressSize.sm}
     568           12 :                               className="pf-v6-u-mb-xs" />
     569           12 :                 </GridItem>
     570              : 
     571           12 :                 <GridItem span={3}>{cancelButton}</GridItem>
     572              : 
     573           12 :                 <GridItem span={12}>
     574           12 :                     <Switch id="reboot-after" isChecked={rebootAfter}
     575           12 :                             label={ _("Reboot after completion") }
     576           12 :                             onChange={setRebootAfter} />
     577           12 :                 </GridItem>
     578              : 
     579           12 :                 <GridItem span={12} className="update-log">
     580            3 :                     <ExpandableSection toggleText={_("View update log")} onToggle={() => {
     581              :                         // always scroll down on expansion
     582            3 :                         const log = document.getElementById("update-log");
     583            3 :                         log.scrollTop = log.scrollHeight;
     584            3 :                     }}>
     585           12 :                         <div id="update-log" className="update-log-content">
     586           12 :                             <table>
     587           12 :                                 <tbody>
     588            9 :                                     { actions.slice(0, -1).map((action, i) => (
     589            9 :                                         <tr key={action.packageId + i}>
     590            1 :                                             <th>{PK_STATUS_LOG_STRINGS[action.status] || PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_UPDATE]}</th>
     591            9 :                                             <td>{formatPackageId(action.packageId)}</td>
     592            9 :                                         </tr>)) }
     593           12 :                                 </tbody>
     594           12 :                             </table>
     595           12 :                         </div>
     596           12 :                     </ExpandableSection>
     597           12 :                 </GridItem>
     598           12 :             </Grid>
     599           12 :         </div>
     600              :     );
     601           12 : };
     602              : 
     603            2 : const TwoColumnContent = ({ list, flexClassName }) => {
     604            2 :     const half = Math.round(list.length / 2);
     605            2 :     const col1 = list.slice(0, half);
     606            2 :     const col2 = list.slice(half);
     607            2 :     return (
     608            2 :         <Flex className={flexClassName}>
     609            2 :             <FlexItem flex={{ default: 'flex_1' }}>
     610            2 :                 <Content component="ul">
     611            2 :                     {col1.map(item => (<Content component="li" key={item}>{item}</Content>))}
     612            2 :                 </Content>
     613            2 :             </FlexItem>
     614            1 :             {col2.length > 0 && <FlexItem flex={{ default: 'flex_1' }}>
     615            1 :                 <Content component="ul">
     616            0 :                     {col2.map(item => (<Content component="li" key={item}>{item}</Content>))}
     617            1 :                 </Content>
     618            1 :             </FlexItem>}
     619            2 :         </Flex>
     620              :     );
     621            2 : };
     622              : 
     623            2 : const TwoColumnTitle = ({ icon, str }) => {
     624            2 :     return (<>
     625            2 :         {icon}
     626            2 :         <span className="update-success-table-title">
     627            2 :             {str}
     628            2 :         </span>
     629            2 :     </>);
     630            2 : };
     631              : 
     632            9 : const UpdateSuccess = ({ onIgnore, openServiceRestartDialog, openRebootDialog, restart, manual, reboot, checkRestartAvailable, history }) => {
     633            2 :     if (!checkRestartAvailable) {
     634              :         /* tracer is not available any more in RHEL 10; as a special case, if only kpatch and kernel were
     635              :          * updated, don't reboot (as that's their whole raison d'être) */
     636            1 :         const pkgs = Object.keys(history[0]?.packages ?? {});
     637            1 :         const only_kpatch = pkgs.filter(p => p.startsWith("kpatch")).length > 0 &&
     638            0 :                             pkgs.filter(p => !p.startsWith("kernel") && !p.startsWith("kpatch")).length == 0;
     639              : 
     640            1 :         const paragraph = only_kpatch ? null : _("Updated packages may require a reboot to take effect.");
     641            2 :         const actions = only_kpatch
     642            1 :             ? <Button id="ignore" variant="primary" onClick={onIgnore}>{_("Continue")}</Button>
     643            2 :             : <>
     644            2 :                 <Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button>
     645            2 :                 <Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
     646            2 :             </>;
     647              : 
     648            2 :         return (<>
     649            2 :             <EmptyStatePanel icon={RebootingIcon}
     650            2 :                              title={ _("Update was successful") }
     651            2 :                              headingLevel="h5"
     652            2 :                              paragraph={paragraph}
     653            2 :                              secondary={actions} />
     654            2 :             { history[0]?.packages &&
     655            2 :             <div className="flow-list-blank-slate">
     656            2 :                 <ExpandableSection toggleText={_("Package information")}>
     657            2 :                     <PackageList packages={history[0].packages} />
     658            2 :                 </ExpandableSection>
     659            2 :             </div>
     660              :             }
     661            2 :         </>);
     662            2 :     }
     663              : 
     664            8 :     const entries = [];
     665            1 :     if (reboot.length > 0) {
     666            1 :         entries.push({
     667            1 :             columns: [
     668            1 :                 {
     669            1 :                     title: <TwoColumnTitle icon={<RebootingIcon />}
     670            1 :                                            str={cockpit.format(cockpit.ngettext("$0 package needs a system reboot",
     671            1 :                                                                                 "$0 packages need a system reboot",
     672            1 :                                                                                 reboot.length),
     673            1 :                                                                reboot.length)} />
     674            1 :                 },
     675            1 :             ],
     676            1 :             props: { key: "reboot", id: "reboot-row" },
     677            1 :             hasPadding: true,
     678            1 :             expandedContent: <TwoColumnContent list={reboot} />,
     679            1 :         });
     680            1 :     }
     681              : 
     682            2 :     if (restart.length > 0) {
     683            2 :         entries.push({
     684            2 :             columns: [
     685            2 :                 {
     686            2 :                     title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
     687            2 :                                            str={cockpit.format(cockpit.ngettext("$0 service needs to be restarted",
     688            2 :                                                                                 "$0 services need to be restarted",
     689            2 :                                                                                 restart.length),
     690            2 :                                                                restart.length)} />
     691            2 :                 },
     692            2 :             ],
     693            2 :             props: { key: "service", id: "service-row" },
     694            2 :             hasPadding: true,
     695            2 :             expandedContent: <TwoColumnContent list={restart} />,
     696            2 :         });
     697            2 :     }
     698              : 
     699            1 :     if (manual.length > 0) {
     700            1 :         entries.push({
     701            1 :             columns: [
     702            1 :                 {
     703            1 :                     title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
     704            1 :                                            str={_("Some software needs to be restarted manually")} />
     705            1 :                 }
     706            1 :             ],
     707            1 :             props: { key: "manual", id: "manual-row" },
     708            1 :             hasPadding: true,
     709            1 :             expandedContent: <TwoColumnContent list={manual} />,
     710            1 :         });
     711            1 :     }
     712              : 
     713            8 :     const showReboot = reboot.length > 0 || manual.length > 0;
     714              : 
     715            9 :     return (<>
     716            9 :         <EmptyStatePanel title={ _("Update was successful") }
     717            9 :             headingLevel="h5"
     718            9 :             secondary={
     719            9 :                 <>
     720            2 :                     { entries.length > 0 && <ListingTable aria-label={_("Update Success Table")}
     721            2 :                         columns={[{ title: _("Info") }]}
     722            2 :                         showHeader={false}
     723            2 :                         className="updates-success-table"
     724            2 :                         rows={entries} /> }
     725            9 :                     <div className="update-success-actions">
     726            1 :                         { showReboot && <Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button> }
     727            1 :                         { restart.length > 0 && <Button id="choose-service" variant={showReboot ? "secondary" : "primary"} onClick={openServiceRestartDialog}>{_("Restart services...")}</Button> }
     728            8 :                         { reboot.length > 0 || restart.length > 0 || manual.length > 0
     729            2 :                             ? <Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
     730            8 :                             : <Button id="ignore" variant="primary" onClick={onIgnore}>{_("Continue")}</Button> }
     731            9 :                     </div>
     732            9 :                 </>
     733            9 :             } />
     734            8 :         { history[0]?.packages &&
     735            8 :         <div className="flow-list-blank-slate">
     736            8 :             <ExpandableSection toggleText={_("Package information")}>
     737            8 :                 <PackageList packages={history[0].packages} />
     738            8 :             </ExpandableSection>
     739            8 :         </div>
     740              :         }
     741            9 :     </>);
     742            9 : };
     743              : 
     744           19 : const UpdatesStatus = ({ updates, highestSeverity, timeSinceRefresh, restartPackages, onValueChanged }) => {
     745           19 :     const numUpdates = updates.length;
     746           19 :     const numSecurity = count_security_updates(updates);
     747           19 :     const numRestartServices = restartPackages.daemons.length;
     748           19 :     const numManualSoftware = restartPackages.manual.length;
     749           19 :     const numRebootPackages = restartPackages.reboot.length;
     750           19 :     let lastChecked;
     751              :     // PackageKit returns G_MAXUINT if the db was never checked.
     752           18 :     if (timeSinceRefresh !== null && timeSinceRefresh !== 2 ** 32 - 1)
     753           18 :         lastChecked = cockpit.format(_("Last checked: $0"), timeformat.distanceToNow(new Date().valueOf() - timeSinceRefresh * 1000));
     754              : 
     755           19 :     const notifications = [];
     756           16 :     if (numUpdates > 0) {
     757            4 :         if (numUpdates == numSecurity) {
     758            4 :             const stateStr = cockpit.ngettext("$0 security fix available", "$0 security fixes available", numSecurity);
     759            4 :             notifications.push({
     760            4 :                 id: "security-updates-available",
     761            4 :                 stateStr: cockpit.format(stateStr, numSecurity),
     762            4 :                 icon: getSeverityIcon(highestSeverity),
     763            4 :                 secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
     764            4 :             });
     765            3 :         } else {
     766           15 :             let stateStr = cockpit.ngettext("$0 update available", "$0 updates available", numUpdates);
     767           15 :             if (numSecurity > 0)
     768            4 :                 stateStr += cockpit.ngettext(", including $1 security fix", ", including $1 security fixes", numSecurity);
     769           15 :             notifications.push({
     770           15 :                 id: "updates-available",
     771           15 :                 stateStr: cockpit.format(stateStr, numUpdates, numSecurity),
     772           15 :                 icon: getSeverityIcon(highestSeverity),
     773           15 :                 secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
     774           15 :             });
     775           15 :         }
     776            8 :     } else if (!numRestartServices && !numRebootPackages && !numManualSoftware) {
     777           11 :         notifications.push({
     778           11 :             id: "system-up-to-date",
     779           11 :             stateStr: STATE_HEADINGS.uptodate,
     780           11 :             icon: <CheckIcon color="green" />,
     781           11 :             secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
     782           11 :         });
     783           11 :     }
     784              : 
     785            3 :     if (numRebootPackages > 0) {
     786            3 :         const stateStr = cockpit.ngettext("$0 package needs a system reboot", "$0 packages need a system reboot", numRebootPackages);
     787            3 :         notifications.push({
     788            3 :             id: "packages-need-reboot",
     789            3 :             stateStr: cockpit.format(stateStr, numRebootPackages),
     790            3 :             icon: <RebootingIcon />,
     791            0 :             secondary: <Button variant="danger" onClick={() => onValueChanged("showRebootSystemDialog", true)}>
     792            3 :                 {_("Reboot system...")}
     793            3 :             </Button>
     794            3 :         });
     795            3 :     }
     796              : 
     797            3 :     if (numRestartServices > 0) {
     798            3 :         const stateStr = cockpit.ngettext("$0 service needs to be restarted", "$0 services need to be restarted", numRestartServices);
     799            3 :         notifications.push({
     800            3 :             id: "services-need-restart",
     801            3 :             stateStr: cockpit.format(stateStr, numRestartServices),
     802            3 :             icon: <ProcessAutomationIcon />,
     803            1 :             secondary: <Button variant="primary" onClick={() => onValueChanged("showRestartServicesDialog", true)}>
     804            3 :                 {_("Restart services...")}
     805            3 :             </Button>
     806            3 :         });
     807            3 :     }
     808              : 
     809            3 :     if (numManualSoftware > 0) {
     810            3 :         notifications.push({
     811            3 :             id: "processes-need-restart",
     812            3 :             stateStr: _("Some software needs to be restarted manually"),
     813            3 :             icon: <ProcessAutomationIcon />,
     814            3 :             secondary: <Content component={ContentVariants.small}>{restartPackages.manual.join(", ")}</Content>
     815            3 :         });
     816            3 :     }
     817              : 
     818           19 :     return (<Stack hasGutter>
     819           19 :         { notifications.map(notification => (
     820           19 :             <StackItem key={notification.id}>
     821           19 :                 <Flex flexWrap={{ default: 'nowrap' }} id={notification.id}>
     822           19 :                     <FlexItem>
     823           19 :                         {notification.icon}
     824           19 :                     </FlexItem>
     825           19 :                     <FlexItem>
     826           19 :                         <Stack>
     827           19 :                             <StackItem>
     828           19 :                                 <Content component={ContentVariants.p}>{notification.stateStr}</Content>
     829           19 :                             </StackItem>
     830           19 :                             <StackItem>
     831           19 :                                 { notification.secondary }
     832           19 :                             </StackItem>
     833           19 :                         </Stack>
     834           19 :                     </FlexItem>
     835           19 :                 </Flex>
     836           19 :             </StackItem>
     837           19 :         ))}
     838           19 :     </Stack>);
     839           19 : };
     840              : 
     841           19 : class CardsPage extends React.Component {
     842           19 :     constructor() {
     843           19 :         super();
     844           19 :         this.state = {
     845           19 :             autoupdates_backend: undefined,
     846           19 :         };
     847           19 :     }
     848              : 
     849           19 :     componentDidMount() {
     850           18 :         getBackend(this.props.backend).then(b => { this.setState({ autoupdates_backend: b }) });
     851           19 :     }
     852              : 
     853           19 :     render() {
     854           19 :         const cardContents = [];
     855           19 :         let settingsContent = null;
     856           19 :         const statusContent = <Stack hasGutter>
     857           19 :             <UpdatesStatus key="updates-status"
     858           19 :                                 updates={this.props.updates}
     859           19 :                                 onValueChanged={this.props.onValueChanged}
     860           19 :                                 restartPackages={this.props.restartPackages}
     861           19 :                                 highestSeverity={this.props.highestSeverity}
     862           19 :                                 timeSinceRefresh={this.props.timeSinceRefresh} />
     863           19 :             <KpatchStatus />
     864           19 :         </Stack>;
     865              : 
     866           18 :         if (this.state.autoupdates_backend) {
     867           18 :             settingsContent = <Stack hasGutter>
     868           18 :                 <AutoUpdates privileged={this.props.privileged} packagekit_backend={this.props.backend} initial_backend={this.state.autoupdates_backend} />
     869           18 :                 {cockpit.info.os_release && cockpit.info.os_release?.ID === "rhel" &&
     870            3 :                 <KpatchSettings privileged={this.props.privileged} />
     871              :                 }
     872           18 :             </Stack>;
     873           18 :         }
     874              : 
     875           19 :         cardContents.push({
     876           19 :             id: "status",
     877           18 :             className: settingsContent !== null ? "ct-card-info" : "",
     878           19 :             title: _("Status"),
     879           19 :             actions: (<Tooltip content={_("Check for updates")}>
     880           19 :                 <Button icon={<RedoIcon />} variant="secondary" onClick={this.props.handleRefresh} />
     881           19 :             </Tooltip>),
     882           19 :             body: statusContent,
     883           19 :         });
     884              : 
     885           18 :         if (settingsContent !== null) {
     886           18 :             cardContents.push({
     887           18 :                 id: "settings",
     888           18 :                 className: "ct-card-info",
     889           18 :                 title: _("Settings"),
     890           18 :                 body: settingsContent,
     891           18 :             });
     892           18 :         }
     893              : 
     894           16 :         if (this.props.state === "available") { // automatic updates are not tracked by PackageKit, hide history when they are enabled
     895           16 :             cardContents.push({
     896           16 :                 id: "available-updates",
     897           16 :                 title: _("Available updates"),
     898           16 :                 actions: (<div className="pk-updates--header--actions">
     899           16 :                     {this.props.cockpitUpdate &&
     900            3 :                         <Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
     901            3 :                             <FlexItem>
     902            3 :                                 <ExclamationTriangleIcon className="ct-icon-exclamation-triangle cockpit-update-warning-icon" />
     903            3 :                                 <strong className="cockpit-update-warning-text">
     904            3 :                                     <span className="pf-screen-reader">{_("Danger alert:")}</span>
     905            3 :                                     {_("Web Console will restart")}
     906            3 :                                 </strong>
     907            3 :                             </FlexItem>
     908            3 :                             <FlexItem>
     909            3 :                                 <Popover aria-label="More information popover"
     910            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.")}>
     911            3 :                                     <Button variant="link" isInline>{_("More info...")}</Button>
     912            3 :                                 </Popover>
     913            3 :                             </FlexItem>
     914            3 :                         </Flex>}
     915           16 :                     {this.props.applyKpatches}
     916           16 :                     {this.props.applySecurity}
     917           16 :                     {this.props.applyAll}
     918           16 :                 </div>),
     919           16 :                 containsList: true,
     920           16 :                 body: <UpdatesList updates={this.props.updates} />
     921           16 :             });
     922           16 :         }
     923              : 
     924            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
     925            8 :             cardContents.push({
     926            8 :                 id: "update-history",
     927            8 :                 title: _("Update history"),
     928            8 :                 containsList: true,
     929            8 :                 body: <History packagekit={this.props.history} />
     930            8 :             });
     931            8 :         }
     932              : 
     933           19 :         return cardContents.map(card => {
     934           19 :             return (
     935           19 :                 <Card key={card.id} className={card.className} id={card.id}>
     936           19 :                     <CardHeader actions={{ actions: card.actions }}>
     937           19 :                         <CardTitle component="h2">{card.title}</CardTitle>
     938           19 :                     </CardHeader>
     939           16 :                     <CardBody className={card.containsList ? "contains-list" : null}>
     940           19 :                         {card.body}
     941           19 :                     </CardBody>
     942           19 :                 </Card>
     943              :             );
     944           19 :         });
     945           19 :     }
     946           19 : }
     947              : 
     948           19 : class OsUpdates extends React.Component {
     949           19 :     constructor() {
     950           19 :         super();
     951           19 :         this.state = {
     952           19 :             state: "loading",
     953           19 :             errorMessages: [],
     954           19 :             updates: [],
     955           19 :             timeSinceRefresh: null,
     956           19 :             loadPercent: null,
     957           19 :             cockpitUpdate: false,
     958           19 :             haveOsRepo: null,
     959           19 :             applyTransaction: null,
     960           19 :             applyTransactionProps: {},
     961           19 :             applyActions: [],
     962           19 :             history: [],
     963           19 :             unregistered: false,
     964           19 :             privileged: false,
     965           19 :             autoUpdatesEnabled: undefined,
     966           19 :             restartPackages: { daemons: [], manual: [], reboot: [] },
     967           19 :             checkRestartAvailable: false,
     968           19 :             checkRestartRunning: false,
     969           19 :             showRestartServicesDialog: false,
     970           19 :             showRebootSystemDialog: false,
     971           19 :             backend: "",
     972           19 :             rebootAfterSuccess: false,
     973           19 :             packageManager: null,
     974           19 :         };
     975           19 :         this.handleLoadError = this.handleLoadError.bind(this);
     976           19 :         this.handleRefresh = this.handleRefresh.bind(this);
     977           19 :         this.loadUpdates = this.loadUpdates.bind(this);
     978           19 :         this.onValueChanged = this.onValueChanged.bind(this);
     979           19 :         this.checkNeedsRestart = this.checkNeedsRestart.bind(this);
     980           19 :     }
     981              : 
     982            1 :     onValueChanged(key, value) {
     983            1 :         this.setState({ [key]: value });
     984            1 :     }
     985              : 
     986           19 :     async componentDidMount() {
     987           19 :         this._mounted = true;
     988           19 :         this.checkNeedsRestart();
     989              : 
     990           19 :         superuser.addEventListener("changed", this.handleSuperUserChange);
     991              : 
     992              :         // HACK: force usage of PackageKit backend
     993           19 :         const packageManager = await getPackageManager(true);
     994           19 :         const backend = await packageManager.get_backend();
     995           19 :         this.setState({ packageManager, backend });
     996              : 
     997              :         // check if there is an upgrade in progress already; if so, switch to "applying" state right away
     998           19 :         PK.call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTransactionList", [])
     999           19 :                 .then(([transactions]) => {
    1000           19 :                     if (!this._mounted)
    1001           19 :                         return;
    1002              : 
    1003            1 :                     const promises = transactions.map(transactionPath => PK.call(
    1004            1 :                         transactionPath, "org.freedesktop.DBus.Properties", "Get", [PK.transactionInterface, "Role"]));
    1005              : 
    1006           19 :                     Promise.all(promises)
    1007           19 :                             .then(roles => {
    1008              :                                 // any transaction with UPDATE_PACKAGES role?
    1009            3 :                                 for (let idx = 0; idx < roles.length; ++idx) {
    1010            3 :                                     if (roles[idx][0].v === PK.Enum.ROLE_UPDATE_PACKAGES) {
    1011            3 :                                         this.watchUpdates(transactions[idx]);
    1012            3 :                                         return;
    1013            3 :                                     }
    1014            3 :                                 }
    1015              : 
    1016              :                                 // no running updates found, proceed to showing available updates
    1017           19 :                                 this.initialLoadOrRefresh();
    1018           19 :                             })
    1019            0 :                             .catch(ex => {
    1020            0 :                                 console.warn("GetTransactionList: failed to read PackageKit transaction roles:", ex.message);
    1021              :                                 // be robust, try to continue with loading updates anyway
    1022            0 :                                 this.initialLoadOrRefresh();
    1023            0 :                             });
    1024           19 :                 })
    1025           19 :                 .catch(this.handleLoadError);
    1026           19 :     }
    1027              : 
    1028            0 :     componentWillUnmount() {
    1029            0 :         this._mounted = false;
    1030            0 :         superuser.removeEventListener("changed", this.handleSuperUserChange);
    1031            0 :     }
    1032              : 
    1033           17 :     handleSuperUserChange = () => {
    1034           17 :         this.setState({ privileged: superuser.allowed });
    1035              :         // get out of error state when switching from unprivileged to privileged
    1036           17 :         if (superuser.allowed && this.state.state.indexOf("Error") >= 0)
    1037            4 :             this.loadUpdates();
    1038           17 :     };
    1039              : 
    1040           19 :     checkNeedsRestart() {
    1041           19 :         this.setState({ checkRestartRunning: true });
    1042           19 :         return python.spawn(callTracerScript, undefined, { err: "message", superuser: "require" })
    1043           16 :                 .then(output => {
    1044           16 :                     debug("tracer succeeded, output:", output);
    1045           16 :                     const restartPackages = JSON.parse(output);
    1046              :                     // Filter out duplicates
    1047           16 :                     restartPackages.reboot = deduplicate(shortenCockpitWsInstance(restartPackages.reboot));
    1048           16 :                     restartPackages.daemons = deduplicate(shortenCockpitWsInstance(restartPackages.daemons));
    1049           16 :                     restartPackages.manual = deduplicate(shortenCockpitWsInstance(restartPackages.manual));
    1050           16 :                     debug("tracer parsed restartPackages:", JSON.stringify(restartPackages));
    1051           16 :                     this.setState({ checkRestartAvailable: true, checkRestartRunning: false, restartPackages });
    1052           16 :                 })
    1053            2 :                 .catch((exception, data) => {
    1054              :                     // tracer not installed or supported (like on Arch)? then fall back to dnf needs-restarting
    1055            2 :                     if (exception.message?.includes("ModuleNotFoundError") ||
    1056            0 :                         exception.message?.includes("UnsupportedDistribution")) {
    1057            0 :                         debug('tracer not installed:', JSON.stringify(exception), "trying dnf needs-restarting");
    1058            0 :                         return this.checkDnfNeedsRestarting();
    1059            0 :                     }
    1060              : 
    1061              :                     // log the error except for some common cases: polkit does not allow it
    1062            0 :                     if (exception.problem !== "access-denied" &&
    1063              :                         // or unprivileged session
    1064            0 :                         exception.problem !== "authentication-failed" &&
    1065              :                         // or the session goes away while checking
    1066            0 :                         exception.problem !== "terminated")
    1067            0 :                         console.error(`Tracer failed: "${JSON.stringify(exception)}", data: "${JSON.stringify(data)}"`);
    1068              :                     else
    1069            2 :                         debug('tracer failed for uninteresting reason:', JSON.stringify(exception));
    1070              : 
    1071              :                     // When tracer fails, act like it's not available (demand reboot after every update)
    1072            2 :                     this.setState({
    1073            2 :                         checkRestartAvailable: false,
    1074            2 :                         checkRestartRunning: false,
    1075            2 :                         restartPackages: { reboot: [], daemons: [], manual: [] },
    1076            2 :                     });
    1077            2 :                 });
    1078           19 :     }
    1079              : 
    1080            0 :     checkDnfNeedsRestarting() {
    1081            0 :         const restartPackages = { reboot: [], daemons: [], manual: [] };
    1082              : 
    1083              :         // needs-restarting has no machine-readable API: https://issues.redhat.com/browse/RHEL-56139
    1084              :         // dnf5 needs-restarting also has no machine-readable API: https://github.com/rpm-software-management/dnf5/issues/2341
    1085              :         // --exclude-services was added much later, so check that first
    1086            0 :         return cockpit.spawn(["dnf", "needs-restarting", "--exclude-services"], { err: "message", superuser: "require" })
    1087            0 :                 .then(outManual => {
    1088            0 :                     debug("dnf needs-restarting --exclude-services succeeded:", outManual);
    1089              :                     // format: "pid : argv", e.g. "1234 : mydaemon 3600"
    1090            0 :                     outManual.trim()
    1091            0 :                             .split("\n")
    1092              :                             // HACK: https://issues.redhat.com/browse/RHEL-84657
    1093            0 :                             .filter(line => line.match(/^\d+ : /))
    1094            0 :                             .forEach(line => !line || restartPackages.manual.push(line));
    1095              : 
    1096            0 :                     return Promise.allSettled([
    1097            0 :                         cockpit.spawn(["dnf", "needs-restarting", "--services"], { err: "message", superuser: "require" }),
    1098              :                         // we can't get stdout for a failing process, thus needs script
    1099            0 :                         cockpit.script("! dnf needs-restarting --reboothint", undefined, { err: "message", superuser: "require" }),
    1100            0 :                     ])
    1101            0 :                             .then(([serviceResult, rebootResult]) => {
    1102              :                                 // --services format: one unit name per line
    1103            0 :                                 if (serviceResult.status == 'fulfilled') {
    1104            0 :                                     debug("dnf needs-restarting --services succeeded:", serviceResult.value);
    1105            0 :                                     serviceResult.value.trim()
    1106            0 :                                             .split("\n")
    1107              :                                             // HACK: https://issues.redhat.com/browse/RHEL-84657
    1108            0 :                                             .filter(line => line.endsWith(".service"))
    1109            0 :                                             .forEach(line => restartPackages.daemons.push(line));
    1110            0 :                                 } else {
    1111            0 :                                     console.error("dnf needs-restarting --services failed:", JSON.stringify(serviceResult.reason));
    1112            0 :                                 }
    1113              : 
    1114              :                                 // --reboothint format: "  * kernel-rt" plus header/footer; exit nonzero iff reboot required, inverted above
    1115            0 :                                 if (rebootResult.status == 'fulfilled') {
    1116            0 :                                     debug("dnf needs-restarting --reboothint exited nonzero, wants reboot:", rebootResult.value);
    1117            0 :                                     rebootResult.value.split("\n").forEach(line => {
    1118            0 :                                         if (line.startsWith("  * "))
    1119            0 :                                             restartPackages.reboot.push(line.substring(4));
    1120            0 :                                     });
    1121            0 :                                 } else {
    1122            0 :                                     debug("dnf needs-restarting --reboothint exited zero, no reboot");
    1123            0 :                                 }
    1124              : 
    1125            0 :                                 debug("dnf needs-restarting parsed packages:", JSON.stringify(restartPackages));
    1126            0 :                                 this.setState({ checkRestartAvailable: true, checkRestartRunning: false, restartPackages });
    1127            0 :                             });
    1128            0 :                 })
    1129            0 :                 .catch(ex => {
    1130              :                     // log the error except for some common cases: no dnf
    1131            0 :                     if (ex.problem !== "not-found" &&
    1132              :                         // plugin does not support --exclude-services
    1133            0 :                         !ex.message.includes("usage:") &&
    1134              :                         // polkit does not allow it
    1135            0 :                         ex.problem !== "access-denied" &&
    1136              :                         // or unprivileged session
    1137            0 :                         ex.problem !== "authentication-failed" &&
    1138              :                         // or the session goes away while checking
    1139            0 :                         ex.problem !== "terminated")
    1140            0 :                         console.error("dnf needs-restarting failed:", ex.toString());
    1141              : 
    1142              :                     // act like it's not available (demand reboot after every update)
    1143            0 :                     this.setState({ checkRestartAvailable: false, checkRestartRunning: false, restartPackages });
    1144            0 :                 });
    1145            0 :     }
    1146              : 
    1147            0 :     handleLoadError(ex) {
    1148            0 :         console.warn("loading available updates failed:", JSON.stringify(ex));
    1149              : 
    1150            0 :         if (!this._mounted)
    1151            0 :             return;
    1152              : 
    1153            0 :         if (ex.problem === "not-found" || ex.name?.includes("DBus.Error.ServiceUnknown"))
    1154            0 :             ex = _("PackageKit is not installed");
    1155            0 :         this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, ex.detail || ex.message || ex], state: "loadError" }));
    1156            0 :     }
    1157              : 
    1158           19 :     loadUpdates() {
    1159           19 :         let cockpitUpdate = false;
    1160              : 
    1161           19 :         this.setState({ state: "loading" });
    1162              : 
    1163              :         // check if there is an available version of coreutils; this is a heuristics for unregistered RHEL
    1164              :         // systems to see if they need a subscription to get "proper" OS updates
    1165           19 :         this.state.packageManager.is_available(["coreutils"])
    1166           19 :                 .then((have_coreutils) => this.setState({ haveOsRepo: have_coreutils }),
    1167            0 :                       ex => console.warn("Resolving coreutils failed:", JSON.stringify(ex)))
    1168           19 :                 .then(() => this.state.packageManager.get_updates(true, null).then(updates => {
    1169           19 :                     debug("GetUpdates result:", updates);
    1170           16 :                     if (updates.length) {
    1171           16 :                         for (const update of updates) {
    1172            3 :                             if (update.name === 'cockpit-ws') {
    1173            3 :                                 cockpitUpdate = true;
    1174              :                             // Arch Linux has no cockpit-ws package
    1175            3 :                             } else if (update.name === 'cockpit' && this.state.backend === "alpm") {
    1176            3 :                                 cockpitUpdate = true;
    1177            3 :                             }
    1178           16 :                         }
    1179           16 :                         this.setState({ updates, cockpitUpdate, state: "available" });
    1180            8 :                     } else {
    1181           11 :                         this.setState({ updates: [], state: "uptodate" });
    1182           11 :                     }
    1183           19 :                     this.loadHistory();
    1184           19 :                 }))
    1185           19 :                 .catch(this.handleLoadError);
    1186           19 :     }
    1187              : 
    1188           19 :     async loadHistory() {
    1189           19 :         try {
    1190           19 :             const history = await this.state.packageManager.get_history();
    1191           18 :             this.setState({ history });
    1192            3 :         } catch (exc) {
    1193            3 :             console.warn("Failed to load old transactions (history):", exc);
    1194            3 :         }
    1195           19 :     }
    1196              : 
    1197           18 :     initialLoadOrRefresh() {
    1198            0 :         watchRedHatSubscription(registered => this.setState({ unregistered: !registered }));
    1199              : 
    1200            5 :         cockpit.addEventListener("visibilitychange", () => {
    1201            5 :             if (!cockpit.hidden)
    1202            5 :                 this.loadOrRefresh(false);
    1203            5 :         });
    1204              : 
    1205           18 :         if (!cockpit.hidden)
    1206            2 :             this.loadOrRefresh(true);
    1207              :         else
    1208            6 :             this.loadUpdates();
    1209           18 :     }
    1210              : 
    1211           17 :     async loadOrRefresh(always_load) {
    1212           17 :         try {
    1213           17 :             const seconds = await this.state.packageManager.get_last_refresh_time();
    1214           17 :             this.setState({ timeSinceRefresh: seconds });
    1215              : 
    1216              :             // automatically trigger refresh for ≥ 1 day or if never refreshed
    1217           12 :             if (seconds >= 24 * 3600 || seconds < 0)
    1218            2 :                 this.handleRefresh();
    1219           12 :             else if (always_load)
    1220            9 :                 this.loadUpdates();
    1221            2 :         } catch (exc) {
    1222            2 :             this.handleLoadError(exc);
    1223            2 :         }
    1224           17 :     }
    1225              : 
    1226           12 :     watchUpdates(transactionPath) {
    1227           12 :         this.setState({ state: "applying", applyTransaction: transactionPath, applyTransactionProps: {}, applyActions: [] });
    1228              : 
    1229           12 :         return PK.watchTransaction(transactionPath,
    1230           12 :                                    {
    1231            3 :                                        ErrorCode: (code, details) => this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, details] })),
    1232              : 
    1233           12 :                                        Finished: exit => {
    1234           12 :                                            this.setState({ applyTransaction: null, applyTransactionProps: {}, applyActions: [] });
    1235              : 
    1236           10 :                                            if (exit === PK.Enum.EXIT_SUCCESS) {
    1237           10 :                                                this.setState({ state: "loading", loadPercent: null });
    1238           10 :                                                this.loadHistory().then(() => {
    1239            9 :                                                    if (this.state.checkRestartAvailable) {
    1240            9 :                                                        this.checkNeedsRestart()
    1241            9 :                                                                .finally(() => this.setState({ state: "updateSuccess" }));
    1242            1 :                                                    } else {
    1243            2 :                                                        this.setState({ state: "updateSuccess", loadPercent: null });
    1244            2 :                                                    }
    1245           10 :                                                });
    1246            1 :                                            } else if (exit === PK.Enum.EXIT_CANCELLED) {
    1247            1 :                                                if (this.state.checkRestartAvailable) {
    1248            1 :                                                    this.setState({ state: "loading", loadPercent: null });
    1249            1 :                                                    this.checkNeedsRestart();
    1250            1 :                                                }
    1251            1 :                                                this.loadUpdates();
    1252            1 :                                            } else {
    1253              :                                                // normally we get FAILED here with ErrorCodes; handle unexpected errors to allow for some debugging
    1254            4 :                                                if (exit !== PK.Enum.EXIT_FAILED)
    1255            0 :                                                    this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, cockpit.format(_("PackageKit reported error code $0"), exit)] }));
    1256            4 :                                                this.setState({ state: "updateError" });
    1257            4 :                                            }
    1258           12 :                                        },
    1259              : 
    1260              :                                        // not working/being used in at least Fedora
    1261            0 :                                        RequireRestart: (type, packageId) => console.log("update RequireRestart", type, packageId),
    1262              : 
    1263           10 :                                        Package: (status, packageId) => this.setState(old =>
    1264           10 :                                            ({ applyActions: [...old.applyActions, { status, packageId }] })
    1265           10 :                                        ),
    1266           12 :                                    },
    1267              : 
    1268           12 :                                    notify => this.setState(old =>
    1269           12 :                                        ({ applyTransactionProps: { ...old.applyTransactionProps, ...notify } })
    1270           12 :                                    )
    1271           12 :         )
    1272            0 :                 .catch(ex => {
    1273            0 :                     this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, ex], state: "updateError" }));
    1274            0 :                 });
    1275           12 :     }
    1276              : 
    1277           11 :     applyUpdates(type) {
    1278           11 :         let updates = [...this.state.updates];
    1279           11 :         if (type === UPDATES.SECURITY)
    1280            1 :             updates = updates.filter(update => update.severity === Severity.CRITICAL);
    1281            1 :         if (type === UPDATES.KPATCHES) {
    1282            0 :             updates = updates.filter(update => isKpatchPackage(update.name));
    1283            1 :         }
    1284              : 
    1285           11 :         PK.transaction()
    1286           11 :                 .then(transactionPath => {
    1287           11 :                     this.watchUpdates(transactionPath)
    1288           11 :                             .then(() => {
    1289           11 :                                 PK.update_packages(updates, null, transactionPath)
    1290            0 :                                         .catch(ex => {
    1291              :                                             // We get more useful error messages through ErrorCode or "PackageKit has crashed", so only
    1292              :                                             // show this if we don't have anything else
    1293            0 :                                             this.setState(prevState => ({
    1294            0 :                                                 errorMessages: prevState.errorMessages.length === 0 ? [ex.message] : prevState.errorMessages,
    1295            0 :                                                 state: "updateError",
    1296            0 :                                             }));
    1297            0 :                                         });
    1298           11 :                             });
    1299           11 :                 })
    1300            0 :                 .catch(ex => {
    1301            0 :                     this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, ex.message], state: "updateError" }));
    1302            0 :                 });
    1303           11 :     }
    1304              : 
    1305           19 :     renderContent() {
    1306           19 :         let applySecurity;
    1307           19 :         let applyKpatches;
    1308           19 :         let applyAll;
    1309              : 
    1310              :         /* On unregistered RHEL systems we need some heuristics: If the "main" OS repos (which provide coreutils) require
    1311              :          * a subscription, then point this out and don't show available updates, even if there are some auxiliary
    1312              :          * repositories enabled which don't require subscriptions. But there are a lot of cases (cloud repos, nightly internal
    1313              :          * repos) which don't need a subscription, there it would just be confusing */
    1314            3 :         if (this.state.unregistered && this.state.haveOsRepo === false) {
    1315            3 :             page_status.set_own({
    1316            3 :                 type: "warning",
    1317            3 :                 title: _("Not registered"),
    1318            3 :                 details: {
    1319            3 :                     link: "subscriptions",
    1320            3 :                 }
    1321            3 :             });
    1322              : 
    1323            3 :             return <EmptyStatePanel
    1324            3 :                 title={_("This system is not registered")}
    1325            3 :                 headingLevel="h5"
    1326            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.") }
    1327            3 :                 icon={ExclamationCircleIcon}
    1328            3 :                 action={ _("Register…") }
    1329            0 :                 onAction={ () => cockpit.jump("/subscriptions", cockpit.transport.host) }
    1330            3 :             />;
    1331            3 :         }
    1332              : 
    1333           19 :         switch (this.state.state) {
    1334           19 :         case "loading":
    1335           19 :         case "refreshing":
    1336           19 :         case "locked":
    1337           19 :             page_status.set_own({
    1338           19 :                 type: null,
    1339           19 :                 title: _("Checking for package updates..."),
    1340           19 :                 details: {
    1341           19 :                     link: false,
    1342           19 :                     pficon: "spinner",
    1343           19 :                 }
    1344           19 :             });
    1345              : 
    1346           19 :             if (this.state.loadPercent)
    1347            3 :                 return <Progress value={this.state.loadPercent} title={STATE_HEADINGS[this.state.state]} />;
    1348              :             else
    1349           19 :                 return <EmptyStatePanel loading title={ _("Checking software status")}
    1350           19 :                                         headingLevel="h5"
    1351           19 :                                         paragraph={STATE_HEADINGS[this.state.state]}
    1352           19 :                 />;
    1353              : 
    1354           16 :         case "available":
    1355           16 :         {
    1356           16 :             const num_updates = this.state.updates.length;
    1357           16 :             const num_security_updates = count_security_updates(this.state.updates);
    1358           16 :             const num_kpatches = count_kpatch_updates(this.state.updates);
    1359           16 :             const highest_severity = find_highest_severity(this.state.updates);
    1360              : 
    1361           16 :             applyAll = (
    1362            1 :                 <Button id={num_updates == num_security_updates ? "install-security" : "install-all"} variant="primary" onClick={ () => this.applyUpdates(UPDATES.ALL) }>
    1363           16 :                     { num_updates == num_security_updates
    1364            4 :                         ? _("Install security updates")
    1365           15 :                         : _("Install all updates") }
    1366           16 :                 </Button>);
    1367              : 
    1368            4 :             if (num_security_updates > 0 && num_updates > num_security_updates) {
    1369            4 :                 applySecurity = (
    1370            1 :                     <Button id="install-security" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.SECURITY) }>
    1371            4 :                         {_("Install security updates")}
    1372            4 :                     </Button>);
    1373            4 :             }
    1374              : 
    1375            3 :             if (num_kpatches > 0) {
    1376            3 :                 applyKpatches = (
    1377            0 :                     <Button id="install-kpatches" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.KPATCHES) }>
    1378            3 :                         {_("Install kpatch updates")}
    1379            3 :                     </Button>);
    1380            3 :             }
    1381              : 
    1382           16 :             let text;
    1383           16 :             if (highest_severity == Severity.CRITICAL)
    1384            4 :                 text = _("Security updates available");
    1385           15 :             else if (highest_severity >= Severity.IMPORTANT)
    1386            3 :                 text = _("Bug fix updates available");
    1387            3 :             else if (highest_severity >= Severity.LOW)
    1388            3 :                 text = _("Enhancement updates available");
    1389              :             else
    1390            3 :                 text = _("Updates available");
    1391              : 
    1392           16 :             page_status.set_own({
    1393            4 :                 type: num_security_updates > 0 ? "warning" : "info",
    1394           16 :                 title: text,
    1395           16 :                 details: {
    1396           16 :                     pficon: getPageStatusSeverityIcon(highest_severity)
    1397           16 :                 }
    1398           16 :             });
    1399              : 
    1400           16 :             return (
    1401           16 :                 <>
    1402           16 :                     <PageSection hasBodyWrapper={false}>
    1403           16 :                         <Gallery className='ct-cards-grid' hasGutter>
    1404           16 :                             <CardsPage handleRefresh={this.handleRefresh}
    1405           16 :                                        applySecurity={applySecurity}
    1406           16 :                                        applyAll={applyAll}
    1407           16 :                                        applyKpatches={applyKpatches}
    1408           16 :                                        highestSeverity={highest_severity}
    1409           16 :                                        onValueChanged={this.onValueChanged}
    1410           16 :                                        {...this.state} />
    1411           16 :                         </Gallery>
    1412           16 :                     </PageSection>
    1413           16 :                     { this.state.showRestartServicesDialog &&
    1414            3 :                         <RestartServices
    1415            3 :                             restartPackages={this.state.restartPackages}
    1416            0 :                             close={() => this.setState({ showRestartServicesDialog: false })}
    1417            3 :                             state={this.state.state}
    1418            3 :                             checkNeedsRestart={this.checkNeedsRestart}
    1419            0 :                             onValueChanged={delta => this.setState(delta)}
    1420            3 :                             loadUpdates={this.loadUpdates} />
    1421              :                     }
    1422           16 :                     { this.state.showRebootSystemDialog &&
    1423            0 :                         <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
    1424              :                     }
    1425           16 :                 </>
    1426              :             );
    1427           16 :         }
    1428              : 
    1429            3 :         case "loadError":
    1430            6 :         case "updateError":
    1431            6 :             page_status.set_own({
    1432            6 :                 type: "error",
    1433            6 :                 title: STATE_HEADINGS[this.state.state],
    1434            6 :             });
    1435            6 :             return (
    1436            6 :                 <Stack>
    1437            6 :                     <EmptyStatePanel title={ STATE_HEADINGS[this.state.state] }
    1438            6 :                                     icon={ ExclamationCircleIcon }
    1439            6 :                                     paragraph={
    1440            6 :                                         <Content component={ContentVariants.p}>
    1441            6 :                                             {_("Please resolve the issue and reload this page.")}
    1442            6 :                                         </Content>
    1443              :                                     }
    1444            6 :                     />
    1445            6 :                     <CodeBlock className='pf-v6-u-mx-auto error-log'>
    1446            6 :                         <CodeBlockCode>
    1447            6 :                             {this.state.errorMessages
    1448            0 :                                     .filter((m, index) => index == 0 || m != this.state.errorMessages[index - 1])
    1449            3 :                                     .map(m => <span key={m}>{m}</span>)}
    1450            6 :                         </CodeBlockCode>
    1451            6 :                     </CodeBlock>
    1452            6 :                 </Stack>
    1453              :             );
    1454              : 
    1455           13 :         case "applying":
    1456           13 :             page_status.set_own(null);
    1457           13 :             return <ApplyUpdates transactionProps={this.state.applyTransactionProps}
    1458           13 :                                  actions={this.state.applyActions}
    1459            0 :                                  onCancel={ () => PK.call(this.state.applyTransaction, PK.transactionInterface, "Cancel", []) }
    1460           13 :                                  rebootAfter={this.state.rebootAfterSuccess}
    1461            1 :                                  setRebootAfter={ (_event, enabled) => this.setState({ rebootAfterSuccess: enabled }) }
    1462           13 :             />;
    1463              : 
    1464           11 :         case "updateSuccess": {
    1465            4 :             if (this.state.rebootAfterSuccess) {
    1466            4 :                 this.setState({ state: "restart" });
    1467            4 :                 cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require" });
    1468            4 :                 return null;
    1469            4 :             }
    1470              : 
    1471           10 :             let warningTitle;
    1472            4 :             if (!this.state.checkRestartAvailable) {
    1473            4 :                 warningTitle = _("Reboot recommended");
    1474            3 :             } else {
    1475            9 :                 if (this.state.restartPackages.reboot.length > 0)
    1476            3 :                     warningTitle = cockpit.ngettext("A package needs a system reboot for the updates to take effect:",
    1477            3 :                                                     "Some packages need a system reboot for the updates to take effect:",
    1478            3 :                                                     this.state.restartPackages.reboot.length);
    1479            9 :                 else if (this.state.restartPackages.daemons.length > 0)
    1480            4 :                     warningTitle = cockpit.ngettext("A service needs to be restarted for the updates to take effect:",
    1481            4 :                                                     "Some services need to be restarted for the updates to take effect:",
    1482            4 :                                                     this.state.restartPackages.daemons.length);
    1483            9 :                 else if (this.state.restartPackages.manual.length > 0)
    1484            3 :                     warningTitle = _("Some software needs to be restarted manually");
    1485            9 :             }
    1486              : 
    1487            5 :             if (warningTitle) {
    1488            5 :                 page_status.set_own({
    1489            5 :                     type: "warning",
    1490            5 :                     title: warningTitle
    1491            5 :                 });
    1492            5 :             }
    1493              : 
    1494           10 :             return (
    1495           10 :                 <>
    1496           10 :                     <UpdateSuccess onIgnore={this.loadUpdates}
    1497            1 :                         openServiceRestartDialog={() => this.setState({ showRestartServicesDialog: true })}
    1498            0 :                         openRebootDialog={() => this.setState({ showRebootSystemDialog: true })}
    1499           10 :                         restart={this.state.restartPackages.daemons}
    1500           10 :                         manual={this.state.restartPackages.manual}
    1501           10 :                         reboot={this.state.restartPackages.reboot}
    1502           10 :                         checkRestartAvailable={this.state.checkRestartAvailable}
    1503           10 :                         history={this.state.history} />
    1504           10 :                     { this.state.showRebootSystemDialog &&
    1505            0 :                         <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
    1506              :                     }
    1507           11 :                     { this.state.showRestartServicesDialog &&
    1508            4 :                         <RestartServices restartPackages={this.state.restartPackages}
    1509            0 :                             close={() => this.setState({ showRestartServicesDialog: false })}
    1510            4 :                             state={this.state.state}
    1511            4 :                             checkNeedsRestart={this.checkNeedsRestart}
    1512            0 :                             onValueChanged={delta => this.setState(delta)}
    1513            4 :                             loadUpdates={this.loadUpdates} />
    1514              :                     }
    1515           11 :                 </>
    1516              :             );
    1517           11 :         }
    1518              : 
    1519            4 :         case "restart":
    1520            4 :             page_status.set_own(null);
    1521            4 :             return <EmptyStatePanel loading title={ _("Restarting") }
    1522            4 :                                     headingLevel="h5"
    1523            4 :                                     paragraph={ _("Your server will close the connection soon. You can reconnect after it has restarted.") }
    1524            4 :             />;
    1525              : 
    1526           11 :         case "uptodate":
    1527           11 :         {
    1528           11 :             page_status.set_own({
    1529           11 :                 title: STATE_HEADINGS[this.state.state],
    1530           11 :                 details: {
    1531           11 :                     link: false,
    1532           11 :                     pficon: "check",
    1533           11 :                 }
    1534           11 :             });
    1535              : 
    1536           11 :             return (
    1537           11 :                 <PageSection hasBodyWrapper={false}>
    1538           11 :                     <Gallery className='ct-cards-grid' hasGutter>
    1539           11 :                         <CardsPage onValueChanged={this.onValueChanged} handleRefresh={this.handleRefresh} {...this.state} />
    1540           11 :                     </Gallery>
    1541           11 :                     { this.state.showRestartServicesDialog &&
    1542            3 :                     <RestartServices restartPackages={this.state.restartPackages}
    1543            1 :                                      close={() => this.setState({ showRestartServicesDialog: false })}
    1544            3 :                                      state={this.state.state}
    1545            3 :                                      checkNeedsRestart={this.checkNeedsRestart}
    1546            1 :                                      onValueChanged={delta => this.setState(delta)}
    1547            3 :                                      loadUpdates={this.loadUpdates} />
    1548              :                     }
    1549           11 :                     { this.state.showRebootSystemDialog &&
    1550            0 :                     <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
    1551              :                     }
    1552           11 :                 </PageSection>
    1553              :             );
    1554           11 :         }
    1555              : 
    1556            3 :         default:
    1557            3 :             page_status.set_own(null);
    1558            3 :             return null;
    1559           19 :         }
    1560           19 :     }
    1561              : 
    1562           10 :     handleRefresh() {
    1563           10 :         this.setState({ state: "refreshing", loadPercent: null });
    1564           10 :         this.state.packageManager.refresh(true, data => this.setState({ loadPercent: data.percentage }))
    1565           10 :                 .then(() => {
    1566           10 :                     if (this._mounted === false)
    1567           10 :                         return;
    1568              : 
    1569           10 :                     this.setState({ timeSinceRefresh: 0 });
    1570           10 :                     this.loadUpdates();
    1571           10 :                 })
    1572           10 :                 .catch(this.handleLoadError);
    1573           10 :     }
    1574              : 
    1575           19 :     render() {
    1576           19 :         let content = this.renderContent();
    1577           19 :         if (!["available", "uptodate"].includes(this.state.state))
    1578           19 :             content = <PageSection hasBodyWrapper={false}>{content}</PageSection>;
    1579              : 
    1580           19 :         return (
    1581           19 :             <WithDialogs>
    1582           19 :                 <Page className="pf-m-no-sidebar">
    1583           19 :                     {content}
    1584           19 :                 </Page>
    1585           19 :             </WithDialogs>
    1586              :         );
    1587           19 :     }
    1588           19 : }
    1589              : 
    1590           19 : document.addEventListener("DOMContentLoaded", async () => {
    1591           19 :     init();
    1592              : 
    1593           19 :     try {
    1594           19 :         await cockpit.init();
    1595            3 :     } catch (exp) {
    1596              :         /* Remove this when every beiboot scenario has Cockpit 336 */
    1597            3 :         if (exp.problem === 'not-supported') {
    1598            3 :             const os_release = await read_os_release();
    1599            3 :             cockpit.info.os_release = os_release;
    1600            3 :         }
    1601            3 :     }
    1602              : 
    1603           19 :     const root = createRoot(document.getElementById('app'));
    1604           19 :     root.render(<OsUpdates />);
    1605           19 : });
        

Generated by: LCOV version 2.0-1