* Copyright (C) 2017 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import '../lib/patternfly/patternfly-6-cockpit.scss';
import 'polyfills';
import 'cockpit-dark-theme';
import cockpit from "cockpit";
import React from "react";
import { createRoot } from 'react-dom/client';
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { CodeBlock, CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
import {
Modal, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
import { LabelGroup } from "@patternfly/react-core/dist/esm/components/Label/index.js";
import { Page, PageSection, } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import { Progress, ProgressSize } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
import {
BugIcon,
CheckIcon,
EnhancementIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
RebootingIcon,
RedoIcon,
ProcessAutomationIcon,
SecurityIcon,
} from "@patternfly/react-icons";
import { TableText } from "@patternfly/react-table";
import { Remarkable } from "remarkable";
import { AutoUpdates, getBackend } from "./autoupdates.jsx";
import { KpatchSettings, KpatchStatus } from "./kpatch";
import { History, PackageList } from "./history";
import { page_status } from "notifications";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { ListingTable } from 'cockpit-components-table.jsx';
import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { ShutdownModal } from 'cockpit-components-shutdown.jsx';
import { WithDialogs } from "dialogs.jsx";
import { superuser } from 'superuser';
import * as PK from "packagekit.js";
import * as python from "python.js";
import * as timeformat from "timeformat";
import { debug, watchRedHatSubscription } from './utils';
import { read_os_release } from "os-release.js";
import callTracerScript from './callTracer.py';
import "./updates.scss";
import { Truncate } from '@patternfly/react-core/dist/esm/components/Truncate/index.js';
import { Severity } from '_internal/packagemanager-abstract';
import { getPackageManager } from 'packagemanager';
import { Icon } from '@patternfly/react-core/dist/esm/components/Icon/index.js';
const _ = cockpit.gettext;
const STATE_HEADINGS = {};
const PK_STATUS_STRINGS = {};
const PK_STATUS_LOG_STRINGS = {};
const UPDATES = {
ALL: 0,
SECURITY: 1,
KPATCHES: 2,
};
function init() {
STATE_HEADINGS.loading = _("Loading available updates, please wait...");
STATE_HEADINGS.locked = _("Some other program is currently using the package manager, please wait...");
STATE_HEADINGS.refreshing = _("Refreshing package information");
STATE_HEADINGS.uptodate = _("System is up to date");
STATE_HEADINGS.applying = _("Applying updates");
STATE_HEADINGS.updateError = _("Applying updates failed");
STATE_HEADINGS.loadError = _("Loading available updates failed");
PK_STATUS_STRINGS[PK.Enum.STATUS_DOWNLOAD] = _("Downloading");
PK_STATUS_STRINGS[PK.Enum.STATUS_INSTALL] = _("Installing");
PK_STATUS_STRINGS[PK.Enum.STATUS_UPDATE] = _("Updating");
PK_STATUS_STRINGS[PK.Enum.STATUS_CLEANUP] = _("Setting up");
PK_STATUS_STRINGS[PK.Enum.STATUS_SIGCHECK] = _("Verifying");
PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_DOWNLOAD] = _("Downloaded");
PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_INSTALL] = _("Installed");
PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_UPDATE] = _("Updated");
PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_CLEANUP] = _("Set up");
PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_SIGCHECK] = _("Verified");
}
function deduplicate(list) {
return [...new Set(list)].sort();
}
function insertCommas(list) {
if (list.length <= 1)
return list;
return list.reduce((prev, cur) => [prev, ", ", cur]);
}
function cleanupChangelogLine(text) {
if (!text)
return text;
text = text.replace(/^[-* ]*/, "");
text = text.replace(/^=+\s+/, "").replace(/=+\s*$/, "");
return text.trim();
}
function shortenCockpitWsInstance(list) {
return list.map(item => item.startsWith('cockpit-wsinstance-https') ? 'cockpit-wsinstance-https@.' : item);
}
function count_security_updates(updates) {
let num_security = 0;
for (const u of updates)
if (u.severity === Severity.CRITICAL)
++num_security;
return num_security;
}
function isKpatchPackage(name) {
return name.startsWith("kpatch-patch");
}
function count_kpatch_updates(updates) {
let num_kpatches = 0;
for (const u of updates)
if (isKpatchPackage(u.name))
++num_kpatches;
return num_kpatches;
}
function find_highest_severity(updates) {
let max = Severity.LOW;
for (const u of updates)
if (u.severity > max)
max = u.severity;
return max;
}
* Get appropriate icon for an update severity
*
* info: An Severity level
* secSeverity: If given, further classification of the severity of Severity.CRITICAL from the vendor_urls;
* e. g. "critical", see https://access.redhat.com/security/updates/classification
* Returns: Icon JSX object
*
*/
function getSeverityIcon(info, secSeverity) {
let classes = "severity-icon";
if (secSeverity)
classes += " severity-" + secSeverity;
if (info == Severity.CRITICAL)
return <Icon isInline status="danger"><SecurityIcon aria-label={secSeverity || _("security")} className={classes} /></Icon>;
else if (info >= Severity.IMPORTANT)
return <Icon isInline className='pf-m-important'><BugIcon className={classes} aria-label={_("bug fix")} /></Icon>;
else
return <Icon isInline status="custom"><EnhancementIcon className={classes} aria-label={_("enhancement")} /></Icon>;
}
function getPageStatusSeverityIcon(severity) {
if (severity == Severity.CRITICAL)
return "security";
else if (severity >= Severity.IMPORTANT)
return "bug";
else
return "enhancement";
}
function getSeverityURL(urls) {
if (!urls)
return null;
const knownLevels = ["low", "moderate", "important", "critical"];
let highestIndex = -1;
let highestURL = null;
urls.forEach(value => {
if (value.startsWith("https://access.redhat.com/security/updates/classification/#")) {
const i = knownLevels.indexOf(value.slice(value.indexOf("#") + 1));
if (i > highestIndex) {
highestIndex = i;
highestURL = value;
}
}
});
return highestURL;
}
function customRemarkable() {
const remarkable = new Remarkable();
const orig_link_open = remarkable.renderer.rules.link_open;
remarkable.renderer.rules.link_open = function() {
let result = orig_link_open.apply(null, arguments);
const parser = new DOMParser();
const htmlDocument = parser.parseFromString(result, "text/html");
const links = htmlDocument.getElementsByTagName("a");
if (links.length === 1) {
const href = links[0].getAttribute("href");
result = `<a rel="noopener noreferrer" target="_blank" href="${href}">`;
}
return result;
};
return remarkable;
}
function updateItem(remarkable, info, pkgNames, key) {
let bugs = null;
if (info.bug_urls && info.bug_urls.length) {
bugs = insertCommas(info.bug_urls.map(url => (
<a key={url} rel="noopener noreferrer" target="_blank" href={url}>
{url.match(/[0-9]+$/) || url}
</a>)
));
}
let cves = null;
if (info.cve_urls && info.cve_urls.length) {
cves = insertCommas(info.cve_urls.map(url => (
<a key={url} href={url} rel="noopener noreferrer" target="_blank">
{url.match(/[^/=]+$/)}
</a>)
));
}
let errata = null;
if (info.vendor_urls) {
errata = insertCommas(info.vendor_urls.filter(url => url.indexOf("/errata/") > 0).map(url => (
<a key={url} href={url} rel="noopener noreferrer" target="_blank">
{url.match(/[^/=]+$/)}
</a>)
));
if (!errata.length)
errata = null;
}
let secSeverityURL = getSeverityURL(info.vendor_urls);
const secSeverity = secSeverityURL ? secSeverityURL.slice(secSeverityURL.indexOf("#") + 1) : null;
const icon = getSeverityIcon(info.severity, secSeverity);
let type;
if (info.severity === Severity.CRITICAL) {
if (secSeverityURL)
secSeverityURL = <a rel="noopener noreferrer" target="_blank" href={secSeverityURL}>{secSeverity}</a>;
type = (
<Tooltip id="tip-severity" content={ secSeverity || _("security") }>
<span>
{icon}
{ (info.cve_urls && info.cve_urls.length > 0) ? info.cve_urls.length : "" }
</span>
</Tooltip>
);
} else {
const tip = (info.severity >= Severity.IMPORTANT) ? _("bug fix") : _("enhancement");
type = (
<Tooltip id="tip-severity" content={tip}>
<span>
{icon}
{ bugs ? info.bug_urls.length : "" }
</span>
</Tooltip>
);
}
const pkgList = pkgNames.map((n, index) => (
<Tooltip key={n.name + n.arch} id="tip-summary" content={n.summary + " (" + n.arch + ")"}>
<span>{n.name + (index !== (pkgNames.length - 1) ? ", " : "")}</span>
</Tooltip>)
);
const pkgs = pkgList;
const pkgsTruncated = pkgList.slice(0, 4);
if (pkgList.length > 4)
pkgsTruncated.push(<span key="more">…</span>);
if (pkgNames.some(pkg => isKpatchPackage(pkg.name)))
pkgsTruncated.push(
<LabelGroup key={`${key}-kpatches-labelgroup`} className="kpatches-labelgroup">
{" "}<Badge color="blue">{_("patches")}</Badge>
</LabelGroup>
);
let descriptionFirstLine = (info.description || "").trim();
if (descriptionFirstLine.indexOf("\n") >= 0)
descriptionFirstLine = descriptionFirstLine.slice(0, descriptionFirstLine.indexOf("\n"));
descriptionFirstLine = cleanupChangelogLine(descriptionFirstLine);
let description;
if (info.markdown) {
descriptionFirstLine = <span dangerouslySetInnerHTML={{ __html: remarkable.render(descriptionFirstLine) }} />;
description = <div dangerouslySetInnerHTML={{ __html: remarkable.render(info.description) }} />;
} else {
description = <div className="changelog">{info.description}</div>;
}
const expandedContent = (
<Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>{_("Packages")}</DescriptionListTerm>
<DescriptionListDescription>{pkgs}</DescriptionListDescription>
</DescriptionListGroup>
{ cves
? <DescriptionListGroup>
<DescriptionListTerm>{_("CVE")}</DescriptionListTerm>
<DescriptionListDescription>{cves}</DescriptionListDescription>
</DescriptionListGroup>
: null }
{ secSeverityURL
? <DescriptionListGroup>
<DescriptionListTerm>{_("Severity")}</DescriptionListTerm>
<DescriptionListDescription className="severity">{secSeverityURL}</DescriptionListDescription>
</DescriptionListGroup>
: null }
{ errata
? <DescriptionListGroup>
<DescriptionListTerm>{_("Errata")}</DescriptionListTerm>
<DescriptionListDescription>{errata}</DescriptionListDescription>
</DescriptionListGroup>
: null }
{ bugs
? <DescriptionListGroup>
<DescriptionListTerm>{_("Bugs")}</DescriptionListTerm>
<DescriptionListDescription>{bugs}</DescriptionListDescription>
</DescriptionListGroup>
: null }
</DescriptionList>
<Content>{description}</Content>
</Flex>
);
return {
columns: [
{ title: pkgsTruncated },
{ title: <TableText wrapModifier="truncate">{info.version}</TableText>, props: { className: "version" } },
{ title: <TableText wrapModifier="nowrap">{type}</TableText>, props: { className: "type" } },
{ title: descriptionFirstLine, props: { className: "changelog" } },
],
props: {
key,
className: info.severity === Severity.CRITICAL ? ["error"] : [],
},
hasPadding: true,
expandedContent,
};
}
const UpdatesList = ({ updates }) => {
const remarkable = customRemarkable();
const combined_updates = [];
const sameUpdate = {};
const packageNames = {};
for (const u of updates) {
const hash = u.version + u.description;
const seenId = sameUpdate[hash];
if (seenId) {
packageNames[seenId].push({ name: u.name, arch: u.arch, summary: u.summary });
} else {
sameUpdate[hash] = u.id;
packageNames[u.id] = [{ name: u.name, arch: u.arch, summary: u.summary }];
combined_updates.push(u);
}
}
combined_updates.sort((a, b) => {
if (a.severity === Severity.CRITICAL && b.severity !== Severity.CRITICAL)
return -1;
if (a.severity !== Severity.CRITICAL && b.severity === Severity.CRITICAL)
return 1;
return a.name.localeCompare(b.name);
});
return (
<ListingTable aria-label={_("Available updates")}
gridBreakPoint='grid-lg'
columns={[
{ title: _("Name"), props: { width: 40 } },
{ title: _("Version"), props: { width: 15 } },
{ title: _("Severity"), props: { width: 15 } },
{ title: _("Details"), props: { width: 30 } },
]}
rows={combined_updates.map(update => updateItem(remarkable, update, packageNames[update.id].sort((a, b) => a.name > b.name), update.id))} />
);
};
class RestartServices extends React.Component {
constructor(props) {
super(props);
this.state = {
dialogError: undefined,
dialogErrorDetail: undefined,
restartInProgress: false,
};
this.restart = this.restart.bind(this);
}
restart() {
const daemons = this.props.restartPackages.daemons.sort((a, b) => {
if (a.includes("cockpit") && b.includes("cockpit"))
return 0;
if (a.includes("cockpit"))
return 1;
return a.localeCompare(b);
});
const restarts = daemons.map(service => cockpit.spawn(["systemctl", "restart", service], { superuser: "require", err: "message" }));
this.setState({ restartInProgress: true, dialogError: undefined, dialogErrorDetail: undefined });
Promise.all(restarts)
.then(() => {
this.props.onValueChanged({ restartPackages: { reboot: this.props.restartPackages.reboot, daemons: [], manual: this.props.restartPackages.manual } });
if (this.props.state === "updateSuccess")
this.props.loadUpdates();
this.setState({ restartInProgress: false });
this.props.close();
})
.catch(ex => {
this.setState({ dialogError: _("Failed to restart service"), dialogErrorDetail: ex.message });
this.props.checkNeedsRestart();
});
}
render() {
let body;
if (this.props.checkRestartRunning) {
body = (
<Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
<Spinner size="sm" />
<p>{_("Reloading the state of remaining services")}</p>
</Flex>
);
} else if (this.props.restartPackages.daemons.length > 0) {
body = (<>
{cockpit.ngettext("The following service will be restarted:", "The following services will be restarted:", this.props.restartPackages.daemons.length)}
<TwoColumnContent list={this.props.restartPackages.daemons} flexClassName="restart-services-modal-body" />
</>);
}
return (
<Modal id="restart-services-modal" isOpen
position="top"
variant="medium"
onClose={this.props.close}>
<ModalHeader title={_("Restart services")} />
<ModalBody>
<Stack hasGutter>
{this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />}
<StackItem>{body}</StackItem>
</Stack>
</ModalBody>
<ModalFooter>
{this.props.restartPackages.daemons.includes("cockpit") &&
<Alert variant="warning"
title={_("Web Console will restart")}
isInline>
<p>
{_("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.")}
</p>
</Alert>}
<Button variant='primary'
isDisabled={ this.state.restartInProgress }
onClick={ this.restart }>
{_("Restart services")}
</Button>
<Button variant='link' className='btn-cancel' onClick={ this.props.close }>
{_("Cancel")}
</Button>
</ModalFooter>
</Modal>
);
}
}
const formatPackageId = packageId => {
const pfields = packageId.split(";");
return pfields[0] + " " + pfields[1] + " (" + pfields[2] + ")";
};
const ApplyUpdates = ({ transactionProps, actions, onCancel, rebootAfter, setRebootAfter }) => {
const remain = transactionProps.RemainingTime
? timeformat.distanceToNow(new Date().valueOf() + transactionProps.RemainingTime * 1000)
: null;
let percentage = transactionProps.Percentage || 0;
if (percentage > 100)
percentage = 0;
const log = document.getElementById("update-log");
if (log) {
if (log.scrollHeight - log.clientHeight <= log.scrollTop + 2)
log.scrollTop = log.scrollHeight;
}
const cancelButton = transactionProps.AllowCancel
? <Button variant="secondary" onClick={onCancel} size="sm">{_("Cancel")}</Button>
: null;
if (actions.length === 0 && percentage === 0) {
return <EmptyStatePanel title={ _("Initializing...") }
headingLevel="h5"
secondary={cancelButton}
loading
/>;
}
const lastAction = actions[actions.length - 1];
const curPackage = formatPackageId(lastAction?.packageId || transactionProps.LastPackage || "");
return (
<div className="progress-main-view">
<Grid hasGutter>
<GridItem span={12}>
<div className="progress-description pf-v6-u-display-flex">
<Spinner size="md" isInline />
<strong>{PK_STATUS_STRINGS[lastAction?.status] || PK_STATUS_STRINGS[PK.Enum.STATUS_UPDATE]}</strong>
<Truncate content={curPackage} />
</div>
<Progress title={remain}
aria-label={remain ? _("Time remaining: ") : _("Update progress")}
value={percentage}
size={ProgressSize.sm}
className="pf-v6-u-mb-xs" />
</GridItem>
<GridItem span={3}>{cancelButton}</GridItem>
<GridItem span={12}>
<Switch id="reboot-after" isChecked={rebootAfter}
label={ _("Reboot after completion") }
onChange={setRebootAfter} />
</GridItem>
<GridItem span={12} className="update-log">
<ExpandableSection toggleText={_("View update log")} onToggle={() => {
const log = document.getElementById("update-log");
log.scrollTop = log.scrollHeight;
}}>
<div id="update-log" className="update-log-content">
<table>
<tbody>
{ actions.slice(0, -1).map((action, i) => (
<tr key={action.packageId + i}>
<th>{PK_STATUS_LOG_STRINGS[action.status] || PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_UPDATE]}</th>
<td>{formatPackageId(action.packageId)}</td>
</tr>)) }
</tbody>
</table>
</div>
</ExpandableSection>
</GridItem>
</Grid>
</div>
);
};
const TwoColumnContent = ({ list, flexClassName }) => {
const half = Math.round(list.length / 2);
const col1 = list.slice(0, half);
const col2 = list.slice(half);
return (
<Flex className={flexClassName}>
<FlexItem flex={{ default: 'flex_1' }}>
<Content component="ul">
{col1.map(item => (<Content component="li" key={item}>{item}</Content>))}
</Content>
</FlexItem>
{col2.length > 0 && <FlexItem flex={{ default: 'flex_1' }}>
<Content component="ul">
{col2.map(item => (<Content component="li" key={item}>{item}</Content>))}
</Content>
</FlexItem>}
</Flex>
);
};
const TwoColumnTitle = ({ icon, str }) => {
return (<>
{icon}
<span className="update-success-table-title">
{str}
</span>
</>);
};
const UpdateSuccess = ({ onIgnore, openServiceRestartDialog, openRebootDialog, restart, manual, reboot, checkRestartAvailable, history }) => {
if (!checkRestartAvailable) {
* updated, don't reboot (as that's their whole raison d'être) */
const pkgs = Object.keys(history[0]?.packages ?? {});
const only_kpatch = pkgs.filter(p => p.startsWith("kpatch")).length > 0 &&
pkgs.filter(p => !p.startsWith("kernel") && !p.startsWith("kpatch")).length == 0;
const paragraph = only_kpatch ? null : _("Updated packages may require a reboot to take effect.");
const actions = only_kpatch
? <Button id="ignore" variant="primary" onClick={onIgnore}>{_("Continue")}</Button>
: <>
<Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button>
<Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
</>;
return (<>
<EmptyStatePanel icon={RebootingIcon}
title={ _("Update was successful") }
headingLevel="h5"
paragraph={paragraph}
secondary={actions} />
{ history[0]?.packages &&
<div className="flow-list-blank-slate">
<ExpandableSection toggleText={_("Package information")}>
<PackageList packages={history[0].packages} />
</ExpandableSection>
</div>
}
</>);
}
const entries = [];
if (reboot.length > 0) {
entries.push({
columns: [
{
title: <TwoColumnTitle icon={<RebootingIcon />}
str={cockpit.format(cockpit.ngettext("$0 package needs a system reboot",
"$0 packages need a system reboot",
reboot.length),
reboot.length)} />
},
],
props: { key: "reboot", id: "reboot-row" },
hasPadding: true,
expandedContent: <TwoColumnContent list={reboot} />,
});
}
if (restart.length > 0) {
entries.push({
columns: [
{
title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
str={cockpit.format(cockpit.ngettext("$0 service needs to be restarted",
"$0 services need to be restarted",
restart.length),
restart.length)} />
},
],
props: { key: "service", id: "service-row" },
hasPadding: true,
expandedContent: <TwoColumnContent list={restart} />,
});
}
if (manual.length > 0) {
entries.push({
columns: [
{
title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
str={_("Some software needs to be restarted manually")} />
}
],
props: { key: "manual", id: "manual-row" },
hasPadding: true,
expandedContent: <TwoColumnContent list={manual} />,
});
}
const showReboot = reboot.length > 0 || manual.length > 0;
return (<>
<EmptyStatePanel title={ _("Update was successful") }
headingLevel="h5"
secondary={
<>
{ entries.length > 0 && <ListingTable aria-label={_("Update Success Table")}
columns={[{ title: _("Info") }]}
showHeader={false}
className="updates-success-table"
rows={entries} /> }
<div className="update-success-actions">
{ showReboot && <Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button> }
{ restart.length > 0 && <Button id="choose-service" variant={showReboot ? "secondary" : "primary"} onClick={openServiceRestartDialog}>{_("Restart services...")}</Button> }
{ reboot.length > 0 || restart.length > 0 || manual.length > 0
? <Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
: <Button id="ignore" variant="primary" onClick={onIgnore}>{_("Continue")}</Button> }
</div>
</>
} />
{ history[0]?.packages &&
<div className="flow-list-blank-slate">
<ExpandableSection toggleText={_("Package information")}>
<PackageList packages={history[0].packages} />
</ExpandableSection>
</div>
}
</>);
};
const UpdatesStatus = ({ updates, highestSeverity, timeSinceRefresh, restartPackages, onValueChanged }) => {
const numUpdates = updates.length;
const numSecurity = count_security_updates(updates);
const numRestartServices = restartPackages.daemons.length;
const numManualSoftware = restartPackages.manual.length;
const numRebootPackages = restartPackages.reboot.length;
let lastChecked;
if (timeSinceRefresh !== null && timeSinceRefresh !== 2 ** 32 - 1)
lastChecked = cockpit.format(_("Last checked: $0"), timeformat.distanceToNow(new Date().valueOf() - timeSinceRefresh * 1000));
const notifications = [];
if (numUpdates > 0) {
if (numUpdates == numSecurity) {
const stateStr = cockpit.ngettext("$0 security fix available", "$0 security fixes available", numSecurity);
notifications.push({
id: "security-updates-available",
stateStr: cockpit.format(stateStr, numSecurity),
icon: getSeverityIcon(highestSeverity),
secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
});
} else {
let stateStr = cockpit.ngettext("$0 update available", "$0 updates available", numUpdates);
if (numSecurity > 0)
stateStr += cockpit.ngettext(", including $1 security fix", ", including $1 security fixes", numSecurity);
notifications.push({
id: "updates-available",
stateStr: cockpit.format(stateStr, numUpdates, numSecurity),
icon: getSeverityIcon(highestSeverity),
secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
});
}
} else if (!numRestartServices && !numRebootPackages && !numManualSoftware) {
notifications.push({
id: "system-up-to-date",
stateStr: STATE_HEADINGS.uptodate,
icon: <CheckIcon color="green" />,
secondary: <Content id="last-checked" component={ContentVariants.small}>{lastChecked}</Content>
});
}
if (numRebootPackages > 0) {
const stateStr = cockpit.ngettext("$0 package needs a system reboot", "$0 packages need a system reboot", numRebootPackages);
notifications.push({
id: "packages-need-reboot",
stateStr: cockpit.format(stateStr, numRebootPackages),
icon: <RebootingIcon />,
secondary: <Button variant="danger" onClick={() => onValueChanged("showRebootSystemDialog", true)}>
{_("Reboot system...")}
</Button>
});
}
if (numRestartServices > 0) {
const stateStr = cockpit.ngettext("$0 service needs to be restarted", "$0 services need to be restarted", numRestartServices);
notifications.push({
id: "services-need-restart",
stateStr: cockpit.format(stateStr, numRestartServices),
icon: <ProcessAutomationIcon />,
secondary: <Button variant="primary" onClick={() => onValueChanged("showRestartServicesDialog", true)}>
{_("Restart services...")}
</Button>
});
}
if (numManualSoftware > 0) {
notifications.push({
id: "processes-need-restart",
stateStr: _("Some software needs to be restarted manually"),
icon: <ProcessAutomationIcon />,
secondary: <Content component={ContentVariants.small}>{restartPackages.manual.join(", ")}</Content>
});
}
return (<Stack hasGutter>
{ notifications.map(notification => (
<StackItem key={notification.id}>
<Flex flexWrap={{ default: 'nowrap' }} id={notification.id}>
<FlexItem>
{notification.icon}
</FlexItem>
<FlexItem>
<Stack>
<StackItem>
<Content component={ContentVariants.p}>{notification.stateStr}</Content>
</StackItem>
<StackItem>
{ notification.secondary }
</StackItem>
</Stack>
</FlexItem>
</Flex>
</StackItem>
))}
</Stack>);
};
class CardsPage extends React.Component {
constructor() {
super();
this.state = {
autoupdates_backend: undefined,
};
}
componentDidMount() {
getBackend(this.props.backend).then(b => { this.setState({ autoupdates_backend: b }) });
}
render() {
const cardContents = [];
let settingsContent = null;
const statusContent = <Stack hasGutter>
<UpdatesStatus key="updates-status"
updates={this.props.updates}
onValueChanged={this.props.onValueChanged}
restartPackages={this.props.restartPackages}
highestSeverity={this.props.highestSeverity}
timeSinceRefresh={this.props.timeSinceRefresh} />
<KpatchStatus />
</Stack>;
if (this.state.autoupdates_backend) {
settingsContent = <Stack hasGutter>
<AutoUpdates privileged={this.props.privileged} packagekit_backend={this.props.backend} initial_backend={this.state.autoupdates_backend} />
{cockpit.info.os_release && cockpit.info.os_release?.ID === "rhel" &&
<KpatchSettings privileged={this.props.privileged} />
}
</Stack>;
}
cardContents.push({
id: "status",
className: settingsContent !== null ? "ct-card-info" : "",
title: _("Status"),
actions: (<Tooltip content={_("Check for updates")}>
<Button icon={<RedoIcon />} variant="secondary" onClick={this.props.handleRefresh} />
</Tooltip>),
body: statusContent,
});
if (settingsContent !== null) {
cardContents.push({
id: "settings",
className: "ct-card-info",
title: _("Settings"),
body: settingsContent,
});
}
if (this.props.state === "available") {
cardContents.push({
id: "available-updates",
title: _("Available updates"),
actions: (<div className="pk-updates--header--actions">
{this.props.cockpitUpdate &&
<Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
<FlexItem>
<ExclamationTriangleIcon className="ct-icon-exclamation-triangle cockpit-update-warning-icon" />
<strong className="cockpit-update-warning-text">
<span className="pf-screen-reader">{_("Danger alert:")}</span>
{_("Web Console will restart")}
</strong>
</FlexItem>
<FlexItem>
<Popover aria-label="More information popover"
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.")}>
<Button variant="link" isInline>{_("More info...")}</Button>
</Popover>
</FlexItem>
</Flex>}
{this.props.applyKpatches}
{this.props.applySecurity}
{this.props.applyAll}
</div>),
containsList: true,
body: <UpdatesList updates={this.props.updates} />
});
}
if ((!this.state.autoupdates_backend || !this.state.autoupdates_backend.enabled) && this.props.history.length > 0) {
cardContents.push({
id: "update-history",
title: _("Update history"),
containsList: true,
body: <History packagekit={this.props.history} />
});
}
return cardContents.map(card => {
return (
<Card key={card.id} className={card.className} id={card.id}>
<CardHeader actions={{ actions: card.actions }}>
<CardTitle component="h2">{card.title}</CardTitle>
</CardHeader>
<CardBody className={card.containsList ? "contains-list" : null}>
{card.body}
</CardBody>
</Card>
);
});
}
}
class OsUpdates extends React.Component {
constructor() {
super();
this.state = {
state: "loading",
errorMessages: [],
updates: [],
timeSinceRefresh: null,
loadPercent: null,
cockpitUpdate: false,
haveOsRepo: null,
applyTransaction: null,
applyTransactionProps: {},
applyActions: [],
history: [],
unregistered: false,
privileged: false,
autoUpdatesEnabled: undefined,
restartPackages: { daemons: [], manual: [], reboot: [] },
checkRestartAvailable: false,
checkRestartRunning: false,
showRestartServicesDialog: false,
showRebootSystemDialog: false,
backend: "",
rebootAfterSuccess: false,
packageManager: null,
};
this.handleLoadError = this.handleLoadError.bind(this);
this.handleRefresh = this.handleRefresh.bind(this);
this.loadUpdates = this.loadUpdates.bind(this);
this.onValueChanged = this.onValueChanged.bind(this);
this.checkNeedsRestart = this.checkNeedsRestart.bind(this);
}
onValueChanged(key, value) {
this.setState({ [key]: value });
}
async componentDidMount() {
this._mounted = true;
this.checkNeedsRestart();
superuser.addEventListener("changed", this.handleSuperUserChange);
const packageManager = await getPackageManager(true);
const backend = await packageManager.get_backend();
this.setState({ packageManager, backend });
PK.call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTransactionList", [])
.then(([transactions]) => {
if (!this._mounted)
return;
const promises = transactions.map(transactionPath => PK.call(
transactionPath, "org.freedesktop.DBus.Properties", "Get", [PK.transactionInterface, "Role"]));
Promise.all(promises)
.then(roles => {
for (let idx = 0; idx < roles.length; ++idx) {
if (roles[idx][0].v === PK.Enum.ROLE_UPDATE_PACKAGES) {
this.watchUpdates(transactions[idx]);
return;
}
}
this.initialLoadOrRefresh();
})
.catch(ex => {
console.warn("GetTransactionList: failed to read PackageKit transaction roles:", ex.message);
this.initialLoadOrRefresh();
});
})
.catch(this.handleLoadError);
}
componentWillUnmount() {
this._mounted = false;
superuser.removeEventListener("changed", this.handleSuperUserChange);
}
handleSuperUserChange = () => {
this.setState({ privileged: superuser.allowed });
if (superuser.allowed && this.state.state.indexOf("Error") >= 0)
this.loadUpdates();
};
checkNeedsRestart() {
this.setState({ checkRestartRunning: true });
return python.spawn(callTracerScript, undefined, { err: "message", superuser: "require" })
.then(output => {
debug("tracer succeeded, output:", output);
const restartPackages = JSON.parse(output);
restartPackages.reboot = deduplicate(shortenCockpitWsInstance(restartPackages.reboot));
restartPackages.daemons = deduplicate(shortenCockpitWsInstance(restartPackages.daemons));
restartPackages.manual = deduplicate(shortenCockpitWsInstance(restartPackages.manual));
debug("tracer parsed restartPackages:", JSON.stringify(restartPackages));
this.setState({ checkRestartAvailable: true, checkRestartRunning: false, restartPackages });
})
.catch((exception, data) => {
if (exception.message?.includes("ModuleNotFoundError") ||
exception.message?.includes("UnsupportedDistribution")) {
debug('tracer not installed:', JSON.stringify(exception), "trying dnf needs-restarting");
return this.checkDnfNeedsRestarting();
}
if (exception.problem !== "access-denied" &&
exception.problem !== "authentication-failed" &&
exception.problem !== "terminated")
console.error(`Tracer failed: "${JSON.stringify(exception)}", data: "${JSON.stringify(data)}"`);
else
debug('tracer failed for uninteresting reason:', JSON.stringify(exception));
this.setState({
checkRestartAvailable: false,
checkRestartRunning: false,
restartPackages: { reboot: [], daemons: [], manual: [] },
});
});
}
checkDnfNeedsRestarting() {
const restartPackages = { reboot: [], daemons: [], manual: [] };
return cockpit.spawn(["dnf", "needs-restarting", "--exclude-services"], { err: "message", superuser: "require" })
.then(outManual => {
debug("dnf needs-restarting --exclude-services succeeded:", outManual);
outManual.trim()
.split("\n")
.filter(line => line.match(/^\d+ : /))
.forEach(line => !line || restartPackages.manual.push(line));
return Promise.allSettled([
cockpit.spawn(["dnf", "needs-restarting", "--services"], { err: "message", superuser: "require" }),
cockpit.script("! dnf needs-restarting --reboothint", undefined, { err: "message", superuser: "require" }),
])
.then(([serviceResult, rebootResult]) => {
if (serviceResult.status == 'fulfilled') {
debug("dnf needs-restarting --services succeeded:", serviceResult.value);
serviceResult.value.trim()
.split("\n")
.filter(line => line.endsWith(".service"))
.forEach(line => restartPackages.daemons.push(line));
} else {
console.error("dnf needs-restarting --services failed:", JSON.stringify(serviceResult.reason));
}
if (rebootResult.status == 'fulfilled') {
debug("dnf needs-restarting --reboothint exited nonzero, wants reboot:", rebootResult.value);
rebootResult.value.split("\n").forEach(line => {
if (line.startsWith(" * "))
restartPackages.reboot.push(line.substring(4));
});
} else {
debug("dnf needs-restarting --reboothint exited zero, no reboot");
}
debug("dnf needs-restarting parsed packages:", JSON.stringify(restartPackages));
this.setState({ checkRestartAvailable: true, checkRestartRunning: false, restartPackages });
});
})
.catch(ex => {
if (ex.problem !== "not-found" &&
!ex.message.includes("usage:") &&
ex.problem !== "access-denied" &&
ex.problem !== "authentication-failed" &&
ex.problem !== "terminated")
console.error("dnf needs-restarting failed:", ex.toString());
this.setState({ checkRestartAvailable: false, checkRestartRunning: false, restartPackages });
});
}
handleLoadError(ex) {
console.warn("loading available updates failed:", JSON.stringify(ex));
if (!this._mounted)
return;
if (ex.problem === "not-found" || ex.name?.includes("DBus.Error.ServiceUnknown"))
ex = _("PackageKit is not installed");
this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, ex.detail || ex.message || ex], state: "loadError" }));
}
loadUpdates() {
let cockpitUpdate = false;
this.setState({ state: "loading" });
this.state.packageManager.is_available(["coreutils"])
.then((have_coreutils) => this.setState({ haveOsRepo: have_coreutils }),
ex => console.warn("Resolving coreutils failed:", JSON.stringify(ex)))
.then(() => this.state.packageManager.get_updates(true, null).then(updates => {
debug("GetUpdates result:", updates);
if (updates.length) {
for (const update of updates) {
if (update.name === 'cockpit-ws') {
cockpitUpdate = true;
} else if (update.name === 'cockpit' && this.state.backend === "alpm") {
cockpitUpdate = true;
}
}
this.setState({ updates, cockpitUpdate, state: "available" });
} else {
this.setState({ updates: [], state: "uptodate" });
}
this.loadHistory();
}))
.catch(this.handleLoadError);
}
async loadHistory() {
try {
const history = await this.state.packageManager.get_history();
this.setState({ history });
} catch (exc) {
console.warn("Failed to load old transactions (history):", exc);
}
}
initialLoadOrRefresh() {
watchRedHatSubscription(registered => this.setState({ unregistered: !registered }));
cockpit.addEventListener("visibilitychange", () => {
if (!cockpit.hidden)
this.loadOrRefresh(false);
});
if (!cockpit.hidden)
this.loadOrRefresh(true);
else
this.loadUpdates();
}
async loadOrRefresh(always_load) {
try {
const seconds = await this.state.packageManager.get_last_refresh_time();
this.setState({ timeSinceRefresh: seconds });
if (seconds >= 24 * 3600 || seconds < 0)
this.handleRefresh();
else if (always_load)
this.loadUpdates();
} catch (exc) {
this.handleLoadError(exc);
}
}
watchUpdates(transactionPath) {
this.setState({ state: "applying", applyTransaction: transactionPath, applyTransactionProps: {}, applyActions: [] });
return PK.watchTransaction(transactionPath,
{
ErrorCode: (code, details) => this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, details] })),
Finished: exit => {
this.setState({ applyTransaction: null, applyTransactionProps: {}, applyActions: [] });
if (exit === PK.Enum.EXIT_SUCCESS) {
this.setState({ state: "loading", loadPercent: null });
this.loadHistory().then(() => {
if (this.state.checkRestartAvailable) {
this.checkNeedsRestart()
.finally(() => this.setState({ state: "updateSuccess" }));
} else {
this.setState({ state: "updateSuccess", loadPercent: null });
}
});
} else if (exit === PK.Enum.EXIT_CANCELLED) {
if (this.state.checkRestartAvailable) {
this.setState({ state: "loading", loadPercent: null });
this.checkNeedsRestart();
}
this.loadUpdates();
} else {
if (exit !== PK.Enum.EXIT_FAILED)
this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, cockpit.format(_("PackageKit reported error code $0"), exit)] }));
this.setState({ state: "updateError" });
}
},
RequireRestart: (type, packageId) => console.log("update RequireRestart", type, packageId),
Package: (status, packageId) => this.setState(old =>
({ applyActions: [...old.applyActions, { status, packageId }] })
),
},
notify => this.setState(old =>
({ applyTransactionProps: { ...old.applyTransactionProps, ...notify } })
)
)
.catch(ex => {
this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, ex], state: "updateError" }));
});
}
applyUpdates(type) {
let updates = [...this.state.updates];
if (type === UPDATES.SECURITY)
updates = updates.filter(update => update.severity === Severity.CRITICAL);
if (type === UPDATES.KPATCHES) {
updates = updates.filter(update => isKpatchPackage(update.name));
}
PK.transaction()
.then(transactionPath => {
this.watchUpdates(transactionPath)
.then(() => {
PK.update_packages(updates, null, transactionPath)
.catch(ex => {
this.setState(prevState => ({
errorMessages: prevState.errorMessages.length === 0 ? [ex.message] : prevState.errorMessages,
state: "updateError",
}));
});
});
})
.catch(ex => {
this.setState(prevState => ({ errorMessages: [...prevState.errorMessages, ex.message], state: "updateError" }));
});
}
renderContent() {
let applySecurity;
let applyKpatches;
let applyAll;
* a subscription, then point this out and don't show available updates, even if there are some auxiliary
* repositories enabled which don't require subscriptions. But there are a lot of cases (cloud repos, nightly internal
* repos) which don't need a subscription, there it would just be confusing */
if (this.state.unregistered && this.state.haveOsRepo === false) {
page_status.set_own({
type: "warning",
title: _("Not registered"),
details: {
link: "subscriptions",
}
});
return <EmptyStatePanel
title={_("This system is not registered")}
headingLevel="h5"
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.") }
icon={ExclamationCircleIcon}
action={ _("Register…") }
onAction={ () => cockpit.jump("/subscriptions", cockpit.transport.host) }
/>;
}
switch (this.state.state) {
case "loading":
case "refreshing":
case "locked":
page_status.set_own({
type: null,
title: _("Checking for package updates..."),
details: {
link: false,
pficon: "spinner",
}
});
if (this.state.loadPercent)
return <Progress value={this.state.loadPercent} title={STATE_HEADINGS[this.state.state]} />;
else
return <EmptyStatePanel loading title={ _("Checking software status")}
headingLevel="h5"
paragraph={STATE_HEADINGS[this.state.state]}
/>;
case "available":
{
const num_updates = this.state.updates.length;
const num_security_updates = count_security_updates(this.state.updates);
const num_kpatches = count_kpatch_updates(this.state.updates);
const highest_severity = find_highest_severity(this.state.updates);
applyAll = (
<Button id={num_updates == num_security_updates ? "install-security" : "install-all"} variant="primary" onClick={ () => this.applyUpdates(UPDATES.ALL) }>
{ num_updates == num_security_updates
? _("Install security updates")
: _("Install all updates") }
</Button>);
if (num_security_updates > 0 && num_updates > num_security_updates) {
applySecurity = (
<Button id="install-security" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.SECURITY) }>
{_("Install security updates")}
</Button>);
}
if (num_kpatches > 0) {
applyKpatches = (
<Button id="install-kpatches" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.KPATCHES) }>
{_("Install kpatch updates")}
</Button>);
}
let text;
if (highest_severity == Severity.CRITICAL)
text = _("Security updates available");
else if (highest_severity >= Severity.IMPORTANT)
text = _("Bug fix updates available");
else if (highest_severity >= Severity.LOW)
text = _("Enhancement updates available");
else
text = _("Updates available");
page_status.set_own({
type: num_security_updates > 0 ? "warning" : "info",
title: text,
details: {
pficon: getPageStatusSeverityIcon(highest_severity)
}
});
return (
<>
<PageSection hasBodyWrapper={false}>
<Gallery className='ct-cards-grid' hasGutter>
<CardsPage handleRefresh={this.handleRefresh}
applySecurity={applySecurity}
applyAll={applyAll}
applyKpatches={applyKpatches}
highestSeverity={highest_severity}
onValueChanged={this.onValueChanged}
{...this.state} />
</Gallery>
</PageSection>
{ this.state.showRestartServicesDialog &&
<RestartServices
restartPackages={this.state.restartPackages}
close={() => this.setState({ showRestartServicesDialog: false })}
state={this.state.state}
checkNeedsRestart={this.checkNeedsRestart}
onValueChanged={delta => this.setState(delta)}
loadUpdates={this.loadUpdates} />
}
{ this.state.showRebootSystemDialog &&
<ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
}
</>
);
}
case "loadError":
case "updateError":
page_status.set_own({
type: "error",
title: STATE_HEADINGS[this.state.state],
});
return (
<Stack>
<EmptyStatePanel title={ STATE_HEADINGS[this.state.state] }
icon={ ExclamationCircleIcon }
paragraph={
<Content component={ContentVariants.p}>
{_("Please resolve the issue and reload this page.")}
</Content>
}
/>
<CodeBlock className='pf-v6-u-mx-auto error-log'>
<CodeBlockCode>
{this.state.errorMessages
.filter((m, index) => index == 0 || m != this.state.errorMessages[index - 1])
.map(m => <span key={m}>{m}</span>)}
</CodeBlockCode>
</CodeBlock>
</Stack>
);
case "applying":
page_status.set_own(null);
return <ApplyUpdates transactionProps={this.state.applyTransactionProps}
actions={this.state.applyActions}
onCancel={ () => PK.call(this.state.applyTransaction, PK.transactionInterface, "Cancel", []) }
rebootAfter={this.state.rebootAfterSuccess}
setRebootAfter={ (_event, enabled) => this.setState({ rebootAfterSuccess: enabled }) }
/>;
case "updateSuccess": {
if (this.state.rebootAfterSuccess) {
this.setState({ state: "restart" });
cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require" });
return null;
}
let warningTitle;
if (!this.state.checkRestartAvailable) {
warningTitle = _("Reboot recommended");
} else {
if (this.state.restartPackages.reboot.length > 0)
warningTitle = cockpit.ngettext("A package needs a system reboot for the updates to take effect:",
"Some packages need a system reboot for the updates to take effect:",
this.state.restartPackages.reboot.length);
else if (this.state.restartPackages.daemons.length > 0)
warningTitle = cockpit.ngettext("A service needs to be restarted for the updates to take effect:",
"Some services need to be restarted for the updates to take effect:",
this.state.restartPackages.daemons.length);
else if (this.state.restartPackages.manual.length > 0)
warningTitle = _("Some software needs to be restarted manually");
}
if (warningTitle) {
page_status.set_own({
type: "warning",
title: warningTitle
});
}
return (
<>
<UpdateSuccess onIgnore={this.loadUpdates}
openServiceRestartDialog={() => this.setState({ showRestartServicesDialog: true })}
openRebootDialog={() => this.setState({ showRebootSystemDialog: true })}
restart={this.state.restartPackages.daemons}
manual={this.state.restartPackages.manual}
reboot={this.state.restartPackages.reboot}
checkRestartAvailable={this.state.checkRestartAvailable}
history={this.state.history} />
{ this.state.showRebootSystemDialog &&
<ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
}
{ this.state.showRestartServicesDialog &&
<RestartServices restartPackages={this.state.restartPackages}
close={() => this.setState({ showRestartServicesDialog: false })}
state={this.state.state}
checkNeedsRestart={this.checkNeedsRestart}
onValueChanged={delta => this.setState(delta)}
loadUpdates={this.loadUpdates} />
}
</>
);
}
case "restart":
page_status.set_own(null);
return <EmptyStatePanel loading title={ _("Restarting") }
headingLevel="h5"
paragraph={ _("Your server will close the connection soon. You can reconnect after it has restarted.") }
/>;
case "uptodate":
{
page_status.set_own({
title: STATE_HEADINGS[this.state.state],
details: {
link: false,
pficon: "check",
}
});
return (
<PageSection hasBodyWrapper={false}>
<Gallery className='ct-cards-grid' hasGutter>
<CardsPage onValueChanged={this.onValueChanged} handleRefresh={this.handleRefresh} {...this.state} />
</Gallery>
{ this.state.showRestartServicesDialog &&
<RestartServices restartPackages={this.state.restartPackages}
close={() => this.setState({ showRestartServicesDialog: false })}
state={this.state.state}
checkNeedsRestart={this.checkNeedsRestart}
onValueChanged={delta => this.setState(delta)}
loadUpdates={this.loadUpdates} />
}
{ this.state.showRebootSystemDialog &&
<ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
}
</PageSection>
);
}
default:
page_status.set_own(null);
return null;
}
}
handleRefresh() {
this.setState({ state: "refreshing", loadPercent: null });
this.state.packageManager.refresh(true, data => this.setState({ loadPercent: data.percentage }))
.then(() => {
if (this._mounted === false)
return;
this.setState({ timeSinceRefresh: 0 });
this.loadUpdates();
})
.catch(this.handleLoadError);
}
render() {
let content = this.renderContent();
if (!["available", "uptodate"].includes(this.state.state))
content = <PageSection hasBodyWrapper={false}>{content}</PageSection>;
return (
<WithDialogs>
<Page className="pf-m-no-sidebar">
{content}
</Page>
</WithDialogs>
);
}
}
document.addEventListener("DOMContentLoaded", async () => {
init();
try {
await cockpit.init();
} catch (exp) {
if (exp.problem === 'not-supported') {
const os_release = await read_os_release();
cockpit.info.os_release = os_release;
}
}
const root = createRoot(document.getElementById('app'));
root.render(<OsUpdates />);
});