mirror-cockpit-project-cockpit/pkg/packagekit/restartservices-dialog.tsx
Jelle van der Waa f2f6ae2a73 packagekit: Don't mutate restartPackages.daemons
Array.prototype.sort mutates the array in-place which is a React
property so modifies the parent's state.
2026-09-08 16:23:32 +02:00

131 lines
5.5 KiB
TypeScript

/*
* Copyright (C) 2026 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import React from "react";
import cockpit from "cockpit";
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core/dist/esm/components/Modal/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 { ModalError } from 'cockpit-components-inline-notification.jsx';
import { TwoColumnContent } from "./two-column";
const _ = cockpit.gettext;
export interface RestartPackages {
reboot: string[];
daemons: string[];
manual: string[];
}
interface RestartServicesDialogProps {
restartPackages: RestartPackages;
close: () => void;
state: string;
checkNeedsRestart: () => void;
onValueChanged: (delta: { restartPackages: RestartPackages }) => void;
loadUpdates: () => void;
checkRestartRunning?: boolean;
}
interface RestartServicesDialogState {
dialogError: string | undefined;
dialogErrorDetail: string | undefined;
restartInProgress: boolean;
}
export class RestartServicesDialog extends React.Component<RestartServicesDialogProps, RestartServicesDialogState> {
constructor(props: RestartServicesDialogProps) {
super(props);
this.state = {
dialogError: undefined,
dialogErrorDetail: undefined,
restartInProgress: false,
};
this.restart = this.restart.bind(this);
}
restart() {
// make sure cockpit package is the last to 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 });
// see what services remain
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} {...(this.state.dialogErrorDetail && { 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>
);
}
}