* Copyright (C) 2019 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import React, { useState } from "react";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
import { Divider } from '@patternfly/react-core/dist/esm/components/Divider/index.js';
import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
import { Card, CardHeader, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/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 } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
import {
AsleepIcon,
BanIcon, ErrorCircleOIcon, OnRunningIcon, OffIcon,
ExclamationCircleIcon,
OkIcon, UserIcon, ThumbtackIcon,
} from "@patternfly/react-icons";
import cockpit from "cockpit";
import s_bus from "./busnames.js";
import { systemd_client, MAX_UINT64 } from "./services.jsx";
import * as timeformat from "timeformat";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { useDialogs, DialogsContext } from "dialogs.jsx";
import { ModalError } from 'cockpit-components-inline-notification.jsx';
import './service-details.scss';
import { KebabDropdown } from "cockpit-components-dropdown";
import { TimerDialog } from "./timer-dialog.jsx";
import { from_boot_usec, from_on_calendar } from "./timer-dialog-helpers.js";
const _ = cockpit.gettext;
const METRICS_POLL_DELAY = 30000;
export const CockpitManagedMarker = "# Managed by Cockpit.\n";
* React template for showing basic dialog for confirming action
* Required props:
* - title
* Title of the dialog
* - message
* Message in the dialog
* - close
* Action to be executed when Cancel button is selected.
* Optional props:
* - confirmText
* Text of the button for confirming the action
* - confirmAction
* Action to be executed when the action is confirmed
*/
const ServiceConfirmDialog = ({ id, title, message, confirmText, confirmAction }) => {
const Dialogs = useDialogs();
return (
<Modal id={id} isOpen
position="top" variant="medium"
onClose={Dialogs.close}>
<ModalHeader title={title} />
<ModalBody>
{message}
</ModalBody>
<ModalFooter>
{ confirmText && confirmAction &&
<Button variant='danger' onClick={confirmAction}>
{confirmText}
</Button>
}
<Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
{ _("Cancel") }
</Button>
</ModalFooter>
</Modal>
);
};
* React template for showing possible service action (in a kebab menu)
* Required props:
* - masked
* Unit is masked
* - active
* Unit is active (running)
* - failed
* Unit has failed
* - isPinned
* Unit is pinned
* - canReload
* Unit can be reloaded
* - actionCallback
* Method for calling unit methods like `UnitStart`
* - fileActionCallback
* Method for calling unit file methods like `EnableUnitFiles`
* - deleteActionCallback
* Method for calling deleting the systemd unit
* - pinUnitCallback
* Method to pin unit
* - disabled
* Button is disabled
*/
const ServiceActions = ({ masked, active, failed, canReload, actionCallback, editActionCallback, deleteActionCallback, fileActionCallback, disabled, isPinned, pinUnitCallback }) => {
const Dialogs = useDialogs();
const actions = [];
if (masked) {
actions.push(
<DropdownItem key="unmask" onClick={() => fileActionCallback("UnmaskUnitFiles", undefined)}>{ _("Allow running (unmask)") }</DropdownItem>
);
} else {
if (active) {
if (canReload) {
actions.push(
<DropdownItem key="reload" onClick={() => actionCallback("ReloadUnit")}>{ _("Reload") }</DropdownItem>
);
}
actions.push(
<DropdownItem key="restart" onClick={() => actionCallback("RestartUnit")}>{ _("Restart") }</DropdownItem>
);
actions.push(
<DropdownItem key="stop" onClick={() => actionCallback("StopUnit")}>{ _("Stop") }</DropdownItem>,
);
} else {
actions.push(
<DropdownItem key="start" onClick={() => actionCallback("StartUnit")}>{ _("Start") }</DropdownItem>
);
}
if (editActionCallback || deleteActionCallback) {
actions.push(<Divider key="modify-unit-divider" />);
}
if (editActionCallback) {
actions.push(
<DropdownItem key="edit" onClick={() => editActionCallback()}>{_("Edit")}</DropdownItem>
);
}
if (deleteActionCallback) {
actions.push(
<DropdownItem key="delete" className="pf-m-danger"
onClick={() => deleteActionCallback()}>{_("Delete")}</DropdownItem>
);
}
if (actions.length > 0) {
actions.push(
<Divider key="divider" />
);
}
if (failed)
actions.push(
<DropdownItem key="reset" onClick={() => actionCallback("ResetFailedUnit", []) }>{ _("Clear 'Failed to start'") }</DropdownItem>
);
const confirm = () => {
Dialogs.show(<ServiceConfirmDialog id="mask-service"
title={ _("Mask service") }
message={ _("Masking service prevents all dependent units from running. This can have bigger impact than anticipated. Please confirm that you want to mask this unit.")}
confirmText={ _("Mask service") }
confirmAction={() => {
fileActionCallback("MaskUnitFiles", false);
if (failed)
actionCallback("ResetFailedUnit", []);
Dialogs.close();
}} />);
};
actions.push(
<DropdownItem key="mask" onClick={confirm}>{ _("Disallow running (mask)") }</DropdownItem>
);
actions.push(<Divider key="pin-divider" />);
actions.push(
<DropdownItem key="pin" onClick={() => pinUnitCallback() }>{isPinned ? _("Unpin unit") : _("Pin unit")}</DropdownItem>
);
}
return (
<KebabDropdown id="service-actions" dropdownItems={actions} isDisabled={disabled} title={ _("Additional actions") } />
);
};
* React template for a service details
* Shows current status and information about the service.
* Enables user to control this unit like starting, enabling, etc. the service.
* Required props:
* - unit
* as returned from systemd org.freedesktop.systemd1.{Unit,Socket}
* D-Bus interface, but with unwrapped variants, and with additional "path"
* property and addTimerProperties()
* - permitted
* True if user can control this unit
* - systemdManager
* Callback for displaying errors
* - isValid
* Method for finding if unit is valid
*/
export class ServiceDetails extends React.Component {
static contextType = DialogsContext;
constructor(props) {
super(props);
this.state = {
waitsAction: false,
waitsFileAction: false,
unit_properties: {},
showDeleteDialog: false,
unitPaths: [],
cockpitManaged: false,
isPinned: this.props.pinnedUnits.includes(this.props.unit.Id),
};
this.onOnOffSwitch = this.onOnOffSwitch.bind(this);
this.unitAction = this.unitAction.bind(this);
this.unitFileAction = this.unitFileAction.bind(this);
this.editTimerAction = this.editTimerAction.bind(this);
this.deleteAction = this.deleteAction.bind(this);
this.deleteTimer = this.deleteTimer.bind(this);
this.pinUnit = this.pinUnit.bind(this);
this.unitType = props.unit.Id.split('.').slice(-1)[0];
this.unitTypeCapitalized = this.unitType.charAt(0).toUpperCase() + this.unitType.slice(1);
this.doMemoryCurrentPolling = this.doMemoryCurrentPolling.bind(this);
if (props.unit.ActiveState === "active") {
this.doMemoryCurrentPolling();
this.interval = setInterval(this.doMemoryCurrentPolling, METRICS_POLL_DELAY);
}
}
componentDidMount() {
if (this.props.unit.FragmentPath?.startsWith("/etc/systemd")) {
cockpit.file(this.props.unit.FragmentPath)
.read()
.then(content => {
if (content?.startsWith(CockpitManagedMarker)) {
this.setState({ cockpitManaged: true });
}
});
}
}
componentDidUpdate(prevProps) {
if (this.props.unit.ActiveState === 'active' && !this.interval) {
this.doMemoryCurrentPolling();
this.interval = setInterval(this.doMemoryCurrentPolling, METRICS_POLL_DELAY);
}
if (this.props.unit.ActiveState === 'inactive' && this.interval) {
this.doMemoryCurrentPolling();
clearInterval(this.interval);
}
}
componentWillUnmount() {
if (this.interval)
clearInterval(this.interval);
}
static getDerivedStateFromProps(nextProps, prevState) {
return {
waitsAction: nextProps.loadingUnits,
waitsFileAction: nextProps.loadingUnits,
};
}
show_note(note) {
const Dialogs = this.context;
Dialogs.show(<ServiceConfirmDialog title={_("Note")} message={note} />);
}
show_error(error) {
const Dialogs = this.context;
Dialogs.show(<ServiceConfirmDialog title={_("Error")} message={error} />);
}
doMemoryCurrentPolling() {
systemd_client[this.props.owner].call(this.props.unit.path,
"org.freedesktop.DBus.Properties", "Get",
["org.freedesktop.systemd1." + this.unitTypeCapitalized, 'MemoryCurrent'])
.then(result => {
this.addUnitProperties(
"MemoryCurrent",
result[0] && result[0].v > 0 && result[0].v < MAX_UINT64 ? result[0].v : null,
);
}, ex => console.log(ex.message));
}
addUnitProperties(prop, value) {
if (prop === "MemoryCurrent" && this.state.unit_properties.MemoryCurrent !== value)
this.setState(prevState => ({ unit_properties: { ...prevState.unit_properties, [prop]: value } }));
}
onOnOffSwitch() {
if (this.props.unit.UnitFileState === "enabled") {
let promise;
if (this.props.unit.ActiveState === "active" || this.props.unit.ActiveState === "activating")
promise = this.unitAction("StopUnit");
else if (this.props.unit.ActiveState === "failed")
promise = this.unitAction("ResetFailedUnit", []);
else
promise = Promise.resolve();
promise.then(() => this.unitFileAction("DisableUnitFiles", undefined));
} else {
this.unitFileAction("EnableUnitFiles", false)
.then(() => {
if (this.props.unit.ActiveState !== "active" && this.props.unit.ActiveState !== "activating")
this.unitAction("StartUnit");
});
}
}
unitAction(method, extra_args, catchExc = true) {
if (extra_args === undefined)
extra_args = ["fail"];
this.setState({ waitsAction: true });
const promise = systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, method, [this.props.unit.Names[0]].concat(extra_args));
if (catchExc) {
return promise.catch(error => {
this.show_error(error.toString());
this.setState({ waitsAction: false });
});
} else {
return promise;
}
}
pinUnit() {
const newPinned = this.state.isPinned
? this.props.pinnedUnits.filter(unitId => unitId !== this.props.unit.Id)
: [...this.props.pinnedUnits, this.props.unit.Id];
localStorage.setItem('systemd:pinnedUnits', JSON.stringify(newPinned));
this.setState(prevState => ({ isPinned: !prevState.isPinned }));
dispatchEvent(new Event('storage'));
}
unitFileAction(method, force, catchExc = true) {
this.setState({ waitsFileAction: true });
const args = [[this.props.unit.Names[0]], false];
if (force !== undefined)
args.push(force === "true");
const promise = systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, method, args)
.then(([results]) => {
if (results.length === 2 && !results[0])
this.show_note(_("This unit is not designed to be enabled explicitly."));
* see https://github.com/systemd/systemd/blob/main/src/systemctl/systemctl.c [enable_unit function]
*/
return systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Reload", null);
});
if (catchExc) {
return promise.catch(error => {
this.show_error(error.toString());
this.setState({ waitsFileAction: false });
});
} else {
return promise;
}
}
editTimerAction() {
async function getCommand(bus, serviceUnitName) {
const serviceDbusPath = await bus.call(s_bus.O_MANAGER, s_bus.I_MANAGER, "LoadUnit", [serviceUnitName]);
const serviceProperties = await bus.call(serviceDbusPath[0], s_bus.I_PROPS, "GetAll", [s_bus.I_SERVICE]);
const exec = serviceProperties[0].ExecStart.v;
if (exec.length !== 1) {
console.warn(`${exec.length} ExecStart= entries were found for ${serviceUnitName} instead of 1`);
return null;
}
const [cmd, args] = exec[0];
if (cmd !== "/bin/sh" || args.length !== 3 || args[1] !== "-c") {
console.warn(`ExecStart= entry is not of the form "/bin/sh -c CMD"`);
return null;
}
return args[2];
}
const Dialogs = this.context;
const timerName = this.props.unit.Id.slice(0, this.props.unit.Id.lastIndexOf("."));
const serviceUnitName = timerName + ".service";
const timerSettings = {
name: timerName,
description: this.props.unit.Description
};
let conditions = null;
if (this.props.unit.TimersMonotonic.length > 0) {
conditions = from_boot_usec(this.props.unit.TimersMonotonic[0][1]);
} else if (this.props.unit.TimersCalendar.length > 0) {
conditions = from_on_calendar(this.props.unit.TimersCalendar.map(item => item[1]));
}
getCommand(systemd_client[this.props.owner], serviceUnitName).then(command => {
const timer = { ...timerSettings, ...conditions, command };
Dialogs.show(<TimerDialog owner={this.props.owner} timer={timer} />);
});
}
deleteAction() {
this.getUnitPaths().then(unitPaths => {
this.setState({ showDeleteDialog: true, unitPaths });
});
}
async getUnitPaths() {
const paths = [this.props.unit.FragmentPath];
await Promise.all(this.props.unit.Triggers.map(async trigger => {
const unitPath = await cockpit.spawn(["systemctl", "show", "--value",
"--property", "FragmentPath", trigger]);
paths.push(unitPath.trim());
})).catch(err => console.error("failed to look up unit details:", err.toString()));
return paths;
}
deleteTimer() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
const promises = [];
if (this.props.unit.ActiveState === "active" || this.props.unit.ActiveState === "activating")
promises.push(this.unitAction("StopUnit", undefined, false));
if (this.props.unit.ActiveState === "failed")
promises.push(this.unitAction("ResetFailedUnit", undefined, false));
if (this.props.unit.UnitFileState === "enabled")
promises.push(this.unitFileAction("DisableUnitFiles", undefined, false));
return Promise.all(promises).then(() => {
const deletions = this.state.unitPaths.filter(path => path.startsWith("/etc/systemd/system"))
.map(path => cockpit.file(path, { superuser: "require" }).replace(null));
return Promise.all(deletions).then(() =>
systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Reload", null)
.then(() => cockpit.jump("/system/services#/?type=timer"))
);
});
}
render() {
const active = this.props.unit.ActiveState === "active" || this.props.unit.ActiveState === "activating";
const enabled = this.props.unit.UnitFileState === "enabled";
const isStatic = this.props.unit.UnitFileState !== "disabled" && !enabled;
const failed = this.props.unit.ActiveState === "failed";
const masked = this.props.unit.LoadState === "masked";
const unit = this.state.unit_properties;
const showAction = this.props.permitted || this.props.owner === "user";
const isCustom = this.props.unit.FragmentPath.startsWith("/etc/systemd/system") && !masked;
const isTimer = (this.unitType === "timer");
const isQuadlet = this.props.unit.SourcePath?.includes("/containers/systemd/");
let status = [];
if (masked) {
status.push(
<div key="masked" className="status-masked">
<Icon>
<BanIcon className="status-icon" />
</Icon>
<span className="status">{ _("Masked") }</span>
<span className="side-note font-xs">{ _("Forbidden from running") }</span>
</div>
);
}
if (!enabled && !active && !masked && !isStatic) {
status.push(
<div key="disabled" className="status-disabled">
<Icon>
<OffIcon className="status-icon" />
</Icon>
<span className="status">{ _("Disabled") }</span>
</div>
);
}
if (failed) {
status.push(
<div key="failed" className="status-failed">
<Icon status="danger">
<ErrorCircleOIcon className="status-icon" />
</Icon>
<span className="status">{ _("Failed to start") }</span>
{ showAction &&
<Button variant="secondary" className="action-button" onClick={() => this.unitAction("StartUnit") }>{ _("Start service") }</Button>
}
</div>
);
}
if (!status.length) {
if (active) {
status.push(
<div key="running" className="status-running">
<Icon status="success">
<OnRunningIcon className="status-icon" />
</Icon>
<span className="status">{ _("Running") }</span>
<span className="side-note font-xs">{ _("Active since ") + timeformat.dateTime(this.props.unit.ActiveEnterTimestamp / 1000) }</span>
</div>
);
} else {
status.push(
<div key="stopped" className="status-stopped">
<Icon>
<OffIcon className="status-icon" />
</Icon>
<span className="status">{ _("Not running") }</span>
</div>
);
}
}
if (isStatic && !masked) {
status.unshift(
<div key="static" className="status-static">
<Icon>
<AsleepIcon className="status-icon" />
</Icon>
<span className="status">{ _("Static") }</span>
{ this.props.unit.WantedBy && this.props.unit.WantedBy.length > 0 &&
<>
<span className="side-note font-xs">{ _("Required by ") }</span>
<ul className="comma-list">
{this.props.unit.WantedBy.map(unit => <li className="font-xs" key={unit}><a href={"#/" + unit}>{unit}</a></li>)}
</ul>
</>
}
</div>
);
}
if (!showAction && this.props.owner !== 'user') {
status.unshift(
<div key="readonly" className="status-readonly">
<Icon>
<UserIcon className="status-icon" />
</Icon>
<span className="status">{ _("Read-only") }</span>
<span className="side-note font-xs">{ _("Requires administration access to edit") }</span>
</div>
);
}
if (enabled) {
status.push(
<div key="enabled" className="status-enabled">
<Icon status="success">
<OkIcon className="status-icon" />
</Icon>
<span className="status">{ _("Automatically starts") }</span>
</div>
);
}
if (this.props.unit.NextRunTime || this.props.unit.LastTriggerTime) {
status.push(
<div className="service-unit-triggers" key="triggers">
{this.props.unit.NextRunTime && <div className="service-unit-next-trigger">{cockpit.format("Next run: $0", this.props.unit.NextRunTime)}</div>}
{this.props.unit.LastTriggerTime && <div className="service-unit-last-trigger">{cockpit.format("Last trigger: $0", this.props.unit.LastTriggerTime)}</div>}
</div>
);
}
if (this.state.waitsAction || this.state.waitsFileAction) {
status = [
<div key="updating" className="status-updating">
<Icon>
<Spinner size="md" className="status-icon" />
</Icon>
<span className="status">{ _("Updating status...") }</span>
</div>
];
}
const tooltipMessage = enabled ? _("Stop and disable") : _("Start and enable");
const hasLoadError = this.props.unit.LoadState !== "loaded" && this.props.unit.LoadState !== "masked";
if (hasLoadError) {
const path = "/system/services" + (this.props.owner === "user" ? "#/?owner=user" : "");
const loadError = this.props.unit.LoadError ? this.props.unit.LoadError[1] : null;
const title = loadError || _("Failed to load unit");
return <EmptyStatePanel
icon={ExclamationCircleIcon}
title={title}
paragraph={this.props.unitId}
action={_("View all services")}
actionVariant="link"
onAction={() => cockpit.jump(path, cockpit.transport.host)}
/>;
}
const triggerRelationships = [
{ Name: _("Triggers"), Units: this.props.unit.Triggers },
{ Name: _("Triggered by"), Units: this.props.unit.TriggeredBy },
];
const relationships = [
{ Name: _("Requires"), Units: this.props.unit.Requires },
{ Name: _("Requisite"), Units: this.props.unit.Requisite },
{ Name: _("Wants"), Units: this.props.unit.Wants },
{ Name: _("Binds to"), Units: this.props.unit.BindsTo },
{ Name: _("Part of"), Units: this.props.unit.PartOf },
{ Name: _("Required by"), Units: this.props.unit.RequiredBy },
{ Name: _("Requisite of"), Units: this.props.unit.RequisiteOf },
{ Name: _("Wanted by"), Units: this.props.unit.WantedBy },
{ Name: _("Bound by"), Units: this.props.unit.BoundBy },
{ Name: _("Consists of"), Units: this.props.unit.ConsistsOf },
{ Name: _("Conflicts"), Units: this.props.unit.Conflicts },
{ Name: _("Conflicted by"), Units: this.props.unit.ConflictedBy },
{ Name: _("Before"), Units: this.props.unit.Before },
{ Name: _("After"), Units: this.props.unit.After },
{ Name: _("On failure"), Units: this.props.unit.OnFailure },
{ Name: _("Propagates reload to"), Units: this.props.unit.PropagatesReloadTo },
{ Name: _("Reload propagated from"), Units: this.props.unit.ReloadPropagatedFrom },
{ Name: _("Joins namespace of"), Units: this.props.unit.JoinsNamespaceOf }
];
const relationshipsToList = rels => {
return rels.filter(rel => rel.Units && rel.Units.length > 0)
.map(rel =>
<DescriptionListGroup key={rel.Name}>
<DescriptionListTerm>{rel.Name}</DescriptionListTerm>
<DescriptionListDescription id={rel.Name.split(" ").join("")}>
<ul className="comma-list">
{rel.Units.map(unit => <li key={unit}><Button isInline variant="link" component="a" href={"#/" + unit + (this.props.owner === "user" ? "?owner=user" : "")} isDisabled={!this.props.isValid(unit)}>{unit}</Button></li>)}
</ul>
</DescriptionListDescription>
</DescriptionListGroup>
);
};
const triggerRelationshipsList = relationshipsToList(triggerRelationships);
const extraRelationshipsList = relationshipsToList(relationships);
const conditions = this.props.unit.Conditions;
const notMetConditions = [];
if (conditions)
conditions.forEach(condition => {
if (condition[4] < 0)
notMetConditions.push(cockpit.format(_("Condition $0=$1 was not met"), condition[0], condition[3]));
});
return (
<Card isPlain id="service-details-unit" className="ct-card">
{ this.state.showDeleteDialog &&
<DeleteModal
name={this.props.unit.Description}
handleCancel={() => this.setState({ showDeleteDialog: false })}
handleDelete={this.deleteTimer}
reason={<Flex>
<p>{_("Deletion will remove the following files:")}</p>
<List>
{this.state.unitPaths.map(item => <ListItem key={item}>{item}</ListItem>)}
</List>
</Flex>
}
/>
}
<CardHeader>
<Flex className="service-top-panel" spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
<CardTitle component="h2" className="service-name">{this.props.unit.Description}</CardTitle>
{this.state.isPinned &&
<Tooltip content={_("Pinned unit")}>
<ThumbtackIcon className='service-thumbtack-icon' />
</Tooltip>}
{ showAction &&
<>
{ !masked && !isStatic &&
<Tooltip id="switch-unit-state" content={tooltipMessage} position={TooltipPosition.right}>
<Switch isChecked={enabled}
aria-label={tooltipMessage}
isDisabled={this.state.waitsAction || this.state.waitsFileAction}
onChange={this.onOnOffSwitch} />
</Tooltip>
}
<ServiceActions { ...{ active, failed, enabled, masked } } canReload={this.props.unit.CanReload}
actionCallback={this.unitAction} fileActionCallback={this.unitFileAction}
editActionCallback={isCustom && isTimer && this.state.cockpitManaged ? this.editTimerAction : null}
deleteActionCallback={isCustom && isTimer ? this.deleteAction : null}
disabled={this.state.waitsAction || this.state.waitsFileAction}
isPinned={this.state.isPinned} pinUnitCallback={this.pinUnit} />
</>
}
</Flex>
</CardHeader>
<CardBody>
<Stack hasGutter>
<DescriptionList isHorizontal>
<DescriptionListGroup>
<DescriptionListTerm>{ _("Status") }</DescriptionListTerm>
<DescriptionListDescription id="statuses">
{ status }
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{ _("Path") }</DescriptionListTerm>
<DescriptionListDescription id="path">{this.props.unit.FragmentPath}</DescriptionListDescription>
</DescriptionListGroup>
{unit.MemoryCurrent
? <DescriptionListGroup>
<DescriptionListTerm>{ _("Memory") }</DescriptionListTerm>
<DescriptionListDescription id="memory">{cockpit.format_bytes(unit.MemoryCurrent)}</DescriptionListDescription>
</DescriptionListGroup>
: null}
{this.props.unit.Listen && this.props.unit.Listen.length && <DescriptionListGroup>
<DescriptionListTerm>{ _("Listen") }</DescriptionListTerm>
<DescriptionListDescription id="listen">
{cockpit.format("$0 ($1)", this.props.unit.Listen[0][1], this.props.unit.Listen[0][0])}
</DescriptionListDescription>
</DescriptionListGroup>}
{isQuadlet && cockpit.manifests?.podman?.capabilities?.includes("service-filtering") && <DescriptionListGroup>
<DescriptionListTerm>{ _("Container") }</DescriptionListTerm>
<DescriptionListDescription id="container">
<Button variant="link" isInline onClick={
() => cockpit.jump(`/podman#/?service=${this.props.unit.Id}`)}>
{_("View Podman container")}
</Button>
</DescriptionListDescription>
</DescriptionListGroup>}
{ notMetConditions.length > 0 &&
<DescriptionListGroup>
<DescriptionListTerm className="failed">{ _("Condition failed") }</DescriptionListTerm>
<DescriptionListDescription id="condition">
{notMetConditions.map(cond => <div key={cond}>{cond}</div>)}
</DescriptionListDescription>
</DescriptionListGroup>
}
{triggerRelationshipsList}
</DescriptionList>
{extraRelationshipsList.length
? <ExpandableSection id="service-details-show-relationships" toggleText={triggerRelationshipsList.length ? _("Show more relationships") : _("Show relationships")}>
<DescriptionList isHorizontal>
{extraRelationshipsList}
</DescriptionList>
</ExpandableSection>
: null}
</Stack>
</CardBody>
</Card>
);
}
}
const DeleteModal = ({ reason, name, handleCancel, handleDelete }) => {
const [inProgress, setInProgress] = useState(false);
const [dialogError, setDialogError] = useState(undefined);
return (
<Modal isOpen
position="top" variant="medium"
onClose={handleCancel}
>
<ModalHeader title={cockpit.format(_("Confirm deletion of $0"), name)}
titleIconVariant="warning"
/>
<ModalBody>
<Stack hasGutter>
{dialogError && <ModalError dialogError={_("Timer deletion failed")} dialogErrorDetail={dialogError} />}
{reason}
</Stack>
</ModalBody>
<ModalFooter>
<Button id="delete-timer-modal-btn" variant="danger" isDisabled={inProgress} isLoading={inProgress}
onClick={() => { setInProgress(true); handleDelete().catch(exc => { setDialogError(exc.message); setInProgress(false) }) }}
>
{_("Delete")}
</Button>
<Button variant="link" isDisabled={inProgress} onClick={handleCancel}>{_("Cancel")}</Button>
</ModalFooter>
</Modal>
);
};