/* * Copyright (C) 2017 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import '../lib/patternfly/patternfly-6-cockpit.scss'; import 'polyfills'; // once per application import 'cockpit-dark-theme'; // once per page import cockpit from "cockpit"; import React from "react"; import { createRoot } from 'react-dom/client'; 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 { 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 Markdown, { RuleType } from "markdown-to-jsx"; import { AutoUpdates, getBackend } from "./autoupdates.jsx"; import { KpatchSettings, KpatchStatus } from "./kpatch"; import { History, PackageList } from "./history"; import { RestartServicesDialog } from './restartservices-dialog'; import { TwoColumnContent, TwoColumnTitle } from "./two-column"; import { page_status } from "notifications"; import { EmptyStatePanel } from "cockpit-components-empty-state.jsx"; import { ListingTable } from 'cockpit-components-table.jsx'; import { ShutdownModal } from 'cockpit-components-shutdown.jsx'; import { WithDialogs } from "dialogs.jsx"; import { superuser } from 'superuser'; 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 { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox/index.js'; import { Truncate } from '@patternfly/react-core/dist/esm/components/Truncate/index.js'; import { Severity, TransactionExitStatus, UpdateProgressType, UpdateError } from '_internal/packagemanager-abstract'; import { getPackageManager } from 'packagemanager'; import { Icon } from '@patternfly/react-core/dist/esm/components/Icon/index.js'; const _ = cockpit.gettext; // "available" heading is built dynamically const STATE_HEADINGS = {}; const STATUS_STRINGS = {}; const STATUS_LOG_STRINGS = {}; const UPDATES = { ALL: 0, SECURITY: 1, KPATCHES: 2, SELECTED: 3, }; /** * PackageKit doesn't provide a way to determine what backends support * partial updates, so we need to manually add the ones we know has partial * package support */ const SUPPORTED_PARTIAL_UPDATE_BACKEND = [ "apt", "dnf", "dnf5", "zypp", ]; export function is_partial_updates_supported(backend) { return SUPPORTED_PARTIAL_UPDATE_BACKEND.includes(backend); } 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"); STATUS_STRINGS[UpdateProgressType.DOWNLOADING] = _("Downloading"); STATUS_STRINGS[UpdateProgressType.INSTALLING] = _("Installing"); STATUS_STRINGS[UpdateProgressType.UPDATING] = _("Updating"); STATUS_STRINGS[UpdateProgressType.CLEANUP] = _("Setting up"); STATUS_STRINGS[UpdateProgressType.SIGCHECK] = _("Verifying"); STATUS_LOG_STRINGS[UpdateProgressType.DOWNLOADING] = _("Downloaded"); STATUS_LOG_STRINGS[UpdateProgressType.INSTALLING] = _("Installed"); STATUS_LOG_STRINGS[UpdateProgressType.UPDATING] = _("Updated"); STATUS_LOG_STRINGS[UpdateProgressType.CLEANUP] = _("Set up"); STATUS_LOG_STRINGS[UpdateProgressType.SIGCHECK] = _("Verified"); } /** @import { UpdateDetail } from "_internal/packagemanager-abstract" */ /** * @typedef SelectedState * @type {object} * @property {Object.} selected - (Un)selected updates. */ /** * @typedef SelectedAction * @type {object} * @property {"ADD" | "REMOVE" | "RESET"} type - Type of reducer action * @property {UpdateDetail[]=} updates - Added/removed items, only used by "ADD" and "REMOVE" */ const SelectedContext = React.createContext({ selected: {} }); /** * Selected store for conveniently updating the state of selected items. * Updating all items naively causes performance issues if there are hundreds of updates */ const SelectedStore = props => { /** * @argument {SelectedState} state * @argument {SelectedAction} action */ const reducer = (state, action) => { switch (action.type) { case "ADD": if (action.updates) { for (const update of action.updates) { state.selected[update.id] = update; } } break; case "REMOVE": if (action.updates) { for (const update of action.updates) { delete state.selected[update.id]; } } break; case "RESET": state.selected = {}; break; default: break; } return { ...state }; }; const [state, dispatch] = React.useReducer(reducer, { selected: {} }); return ; }; /** * @returns {{state: SelectedState, dispatch: (arg: SelectedAction) => void}} */ export const useSelected = () => React.useContext(SelectedContext); /** * @param {{ * onClick: (state: SelectedState) => void * securityOnly: boolean * num_updates: number * }} props; */ const SelectedButton = (props) => { const { state, dispatch } = useSelected(); const { onClick, securityOnly, num_updates, } = props; const buttonId = securityOnly ? "install-security" : "install-all"; const buttonText = () => { const selectedCount = Object.keys(state.selected).length; if (selectedCount === 0 || selectedCount === num_updates) { if (securityOnly) { return _("Install security updates"); } else { return _("Install all updates"); } } else { return `${_("Install selected updates")} (${selectedCount})`; } }; return ( ); }; /** * @param {{ * info: UpdateDetail, * updates: UpdateDetail[], * }} props; */ const SelectedSwitch = (props) => { const { state, dispatch } = useSelected(); const checkboxId = () => `selectable-${props.info.id.replaceAll(';', '-')}`; const dispatchChecked = checked => { if (checked) { dispatch({ type: "ADD", updates: props.updates }); } else { dispatch({ type: "REMOVE", updates: props.updates }); } }; const isChecked = () => { return !!state.selected[props.info.id]; }; return ( dispatchChecked(checked)} /> ); }; /** * @param {{ * updates: UpdateDetail[], * }} props; */ const WebConsoleRestartWarn = (props) => { const { state } = useSelected(); if (calculateSelected(props.updates, state).findIndex((value) => value.id.includes("cockpit-ws")) === -1) return null; return ( {_("Danger alert:")} {_("Web Console will restart")} ); }; /** * @param {UpdateDetail[]} allUpdates * @param {SelectedState} state * @returns {UpdateDetail[]} */ function calculateSelected(allUpdates, state) { const selected = Object.values(state.selected).filter(update => !!update); if (selected.length === 0) { return allUpdates; } else { return selected; } } function deduplicate(list) { return [...new Set(list)].sort(); } // Insert comma strings in between elements of the list. Unlike list.join(",") // this does not stringify the elements, which we need to keep as JSX objects. function insertCommas(list) { if (list.length <= 1) return list; return list.reduce((prev, cur) => [prev, ", ", cur]); } // Fedora changelogs are a wild mix of enumerations or not, headings, etc. // Remove that formatting to avoid an untidy updates overview list function cleanupChangelogLine(text) { if (!text) return text; // enumerations text = text.replace(/^[-* ]*/, ""); // headings text = text.replace(/^=+\s+/, "").replace(/=+\s*$/, ""); return text.trim(); } function isRedHatBasedDistribution() { const os_release = cockpit.info?.os_release; if (!os_release) return false; const ids = [os_release.ID, ...(os_release.ID_LIKE || "").split(" ")].filter(Boolean); return ids.some(id => ["rhel", "fedora", "centos"].includes(id)); } function renderChangelogWithIssueLinks(text) { if (!text) return text; const issuePattern = /\brhbz#(\d+)(?!\w)|\bRHEL-(\d+)(?![-\w])/g; const result = []; let lastIndex = 0; let match; const redHatBased = isRedHatBasedDistribution(); while ((match = issuePattern.exec(text)) !== null) { if (match.index > lastIndex) result.push(text.slice(lastIndex, match.index)); const [, bugzillaId, rhelId] = match; let href = null; let key = null; if (bugzillaId) { href = `https://bugzilla.redhat.com/show_bug.cgi?id=${bugzillaId}`; key = `rhbz-${bugzillaId}-${match.index}`; } else if (rhelId && redHatBased) { href = `https://redhat.atlassian.net/browse/RHEL-${rhelId}`; key = `RHEL-${rhelId}-${match.index}`; } else { result.push(match[0]); lastIndex = issuePattern.lastIndex; continue; } result.push( {match[0]} ); lastIndex = issuePattern.lastIndex; } if (lastIndex === 0) return text; if (lastIndex < text.length) result.push(text.slice(lastIndex)); return result; } // Replace cockpit-wsinstance-https@[long_id] with a shorter string 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 ; else if (info >= Severity.IMPORTANT) return ; else return ; } function getPageStatusSeverityIcon(severity) { if (severity === Severity.CRITICAL) return "security"; else if (severity >= Severity.IMPORTANT) return "bug"; else return "enhancement"; } function getSeverityURL(urls) { if (urls.length === 0) return null; // in ascending severity const knownLevels = ["low", "moderate", "important", "critical"]; let highestIndex = -1; let highestURL = null; // search URLs for highest valid severity; by all means we expect an update to have at most one, but for paranoia.. 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 isSafeUrl(href) { try { const url = new URL(href); return ["https:", "http:"].includes(url.protocol); } catch { return false; } } // Link issue references in Markdown text, except for text that is already part of a link. // markdown-to-jsx renders a node's children inside its next() call, so track link nesting // here; state.inAnchor is only set while parsing, not while rendering. let markdownLinkDepth = 0; function renderMarkdownWithIssueLinks(next, node, _renderChildren, state) { if (node.type === RuleType.link || (node.type === RuleType.htmlBlock && node.tag === "a")) { markdownLinkDepth++; try { return next(); } finally { markdownLinkDepth--; } } if (node.type === RuleType.text && markdownLinkDepth === 0) { const linked = renderChangelogWithIssueLinks(node.text); if (linked !== node.text) return {linked}; } return next(); } const markdownOptions = { overrides: { a: { props: { rel: "noopener noreferrer", target: "_blank", }, }, }, forceInline: true, renderRule: renderMarkdownWithIssueLinks, }; function updateItem(info, pkgNames, key, partialUpdates) { const bug_urls = (info.bug_urls || []).filter(isSafeUrl); const cve_urls = (info.cve_urls || []).filter(isSafeUrl); const vendor_urls = (info.vendor_urls || []).filter(isSafeUrl); let bugs = null; if (bug_urls.length > 0) { // we assume a bug URL ends with a number; if not, show the complete URL bugs = insertCommas(bug_urls.map(url => ( {url.match(/[0-9]+$/) || url} ) )); } let cves = null; if (cve_urls.length > 0) { cves = insertCommas(cve_urls.map(url => ( {url.match(/[^/=]+$/)} ) )); } let errata = null; if (vendor_urls.length > 0) { errata = insertCommas(vendor_urls.filter(url => url.indexOf("/errata/") > 0).map(url => ( {url.match(/[^/=]+$/)} ) )); if (!errata.length) errata = null; // simpler testing below } let secSeverityURL = getSeverityURL(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 = {secSeverity}; type = ( {icon} { cve_urls.length > 0 && cve_urls.length} ); } else { const tip = (info.severity >= Severity.IMPORTANT) ? _("bug fix") : _("enhancement"); type = ( {icon} { bug_urls.length > 0 && bug_urls.length } ); } const pkgList = pkgNames.map((n, index) => ( {n.name + (index !== (pkgNames.length - 1) ? ", " : "")} ) ); const pkgs = pkgList; const pkgsTruncated = pkgList.slice(0, 4); if (pkgList.length > 4) pkgsTruncated.push(…); if (pkgNames.some(pkg => isKpatchPackage(pkg.name))) pkgsTruncated.push( {" "}{_("patches")} ); 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 = {descriptionFirstLine}; description = {info.description}; } else { descriptionFirstLine = renderChangelogWithIssueLinks(descriptionFirstLine); description =
{renderChangelogWithIssueLinks(info.description)}
; } const expandedContent = ( {_("Packages")} {pkgs} { cves ? {_("CVE")} {cves} : null } { secSeverityURL ? {_("Severity")} {secSeverityURL} : null } { errata ? {_("Errata")} {errata} : null } { bugs ? {_("Bugs")} {bugs} : null } {description} ); let selectColumn = []; if (partialUpdates) { selectColumn = [ { title: , props: { className: "select-update" } }, ]; } return { columns: [ ...selectColumn, { title: pkgsTruncated }, { title: {info.version}, props: { className: "version" } }, { title: {type}, props: { className: "type" } }, { title: descriptionFirstLine, props: { className: "changelog" } }, ], props: { key, className: info.severity === Severity.CRITICAL ? ["error"] : [], }, hasPadding: true, expandedContent, }; } const UpdatesList = ({ updates, partialUpdates }) => { const combined_updates = []; // PackageKit doesn"t expose source package names, so group packages with the same version and changelog // create a reverse version+changes → [id] map on iteration const sameUpdate = {}; const packageNames = {}; for (const u of updates) { // did we already see the same version and description? then merge const hash = u.version + u.description; const seenId = sameUpdate[hash]; if (seenId) { packageNames[seenId].push(u); } else { // this is a new update sameUpdate[hash] = u.id; packageNames[u.id] = [u]; combined_updates.push(u); } } // sort security first 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); }); let selectColumn = []; if (partialUpdates) { selectColumn = [ { title: "", props: { hidden: true, 'aria-label': _("Select update") } } ]; } return ( { return updateItem(update, packageNames[update.id].sort((a, b) => a.name > b.name), update.id, partialUpdates, ); }) } /> ); }; const formatPackageId = packageId => { const pfields = packageId.split(";"); return pfields[0] + " " + pfields[1] + " (" + pfields[2] + ")"; }; // actions is a chronological list of { status, packageId } events that happen during applying updates // status: see STATUS_STRINGS const ApplyUpdates = ({ transactionProps, actions, rebootAfter, setRebootAfter }) => { const remain = transactionProps.remaining_time ? timeformat.distanceToNow(new Date().valueOf() + transactionProps.remaining_time * 1000) : null; let percentage = transactionProps.percentage || 0; // PackageKit sets this to 101 initially if (percentage > 100) percentage = 0; // scroll update log to the bottom, if it already is (almost) at the bottom const log = document.getElementById("update-log"); if (log) { if (log.scrollHeight - log.clientHeight <= log.scrollTop + 2) log.scrollTop = log.scrollHeight; } const cancelButton = transactionProps.cancel ? : null; if (actions.length === 0 && percentage === 0) { return ; } const lastAction = actions[actions.length - 1]; // when resuming an upgrade, we did not get any Package signal yet; fall back to LastPackage const curPackage = formatPackageId(lastAction?.packageId || transactionProps.last_package || ""); return (
{STATUS_STRINGS[lastAction?.status] || STATUS_STRINGS[UpdateProgressType.UPDATING]}  
{cancelButton} { // always scroll down on expansion const log = document.getElementById("update-log"); log.scrollTop = log.scrollHeight; }}>
{ actions.slice(0, -1).map((action, i) => ( )) }
{STATUS_LOG_STRINGS[action.status] || STATUS_LOG_STRINGS[UpdateProgressType.UPDATING]} {formatPackageId(action.packageId)}
); }; const UpdateSuccess = ({ onIgnore, openServiceRestartDialog, openRebootDialog, restart, manual, reboot, checkRestartAvailable, history }) => { if (!checkRestartAvailable) { /* tracer is not available any more in RHEL 10; as a special case, if only kpatch and kernel were * 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 ? : <> ; return (<> { history[0]?.packages &&
} ); } const entries = []; if (reboot.length > 0) { entries.push({ columns: [ { title: } 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: , }); } if (restart.length > 0) { entries.push({ columns: [ { title: } 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: , }); } if (manual.length > 0) { entries.push({ columns: [ { title: } str={_("Some software needs to be restarted manually")} /> } ], props: { key: "manual", id: "manual-row" }, hasPadding: true, expandedContent: , }); } const showReboot = reboot.length > 0 || manual.length > 0; return (<> { entries.length > 0 && }
{ showReboot && } { restart.length > 0 && } { reboot.length > 0 || restart.length > 0 || manual.length > 0 ? : }
} /> { history[0]?.packages &&
} ); }; 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; // PackageKit returns G_MAXUINT if the db was never checked. 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: {lastChecked} }); } 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: {lastChecked} }); } } else if (!numRestartServices && !numRebootPackages && !numManualSoftware) { notifications.push({ id: "system-up-to-date", stateStr: STATE_HEADINGS.uptodate, icon: , secondary: {lastChecked} }); } 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: , secondary: }); } 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: , secondary: }); } if (numManualSoftware > 0) { notifications.push({ id: "processes-need-restart", stateStr: _("Some software needs to be restarted manually"), icon: , secondary: {restartPackages.manual.join(", ")} }); } return ( { notifications.map(notification => ( {notification.icon} {notification.stateStr} { notification.secondary } ))} ); }; 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 = ; if (this.state.autoupdates_backend) { settingsContent = {cockpit.info.os_release && cockpit.info.os_release?.ID === "rhel" && } ; } cardContents.push({ id: "status", className: settingsContent !== null ? "ct-card-info" : "", title: _("Status"), actions: ( ); } if (num_kpatches > 0) { applyKpatches = ( ); } 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 ( <> { this.state.showRestartServicesDialog && this.setState({ showRestartServicesDialog: false })} state={this.state.state} checkNeedsRestart={this.checkNeedsRestart} onValueChanged={delta => this.setState(delta)} loadUpdates={this.loadUpdates} /> } { this.state.showRebootSystemDialog && this.setState({ showRebootSystemDialog: false })} /> } ); } case "loadError": case "updateError": page_status.set_own({ type: "error", title: STATE_HEADINGS[this.state.state], }); return ( {_("Please resolve the issue and reload this page.")} } /> {this.state.errorMessages .filter((m, index) => index === 0 || m !== this.state.errorMessages[index - 1]) .map(m => {m})} ); case "applying": page_status.set_own(null); return 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 ( <> 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 && this.setState({ showRebootSystemDialog: false })} /> } { this.state.showRestartServicesDialog && 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 ; case "uptodate": { page_status.set_own({ title: STATE_HEADINGS[this.state.state], details: { link: false, pficon: "check", } }); return ( { this.state.showRestartServicesDialog && this.setState({ showRestartServicesDialog: false })} state={this.state.state} checkNeedsRestart={this.checkNeedsRestart} onValueChanged={delta => this.setState(delta)} loadUpdates={this.loadUpdates} /> } { this.state.showRebootSystemDialog && this.setState({ showRebootSystemDialog: false })} /> } ); } default: page_status.set_own(null); return null; } } handleRefresh() { this.setState({ state: "refreshing", loadPercent: null }); this.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 = {content}; return ( {content} ); } } document.addEventListener("DOMContentLoaded", async () => { init(); try { await cockpit.init(); } catch (exp) { /* Remove this when every beiboot scenario has Cockpit 336 */ 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( ); });