LCOV - code coverage report
Current view: top level - src - Containers.jsx Coverage Total Hit
Test: cockpit-podman Lines: 92.0 % 740 681
Test Date: 2025-05-21 17:35:03

            Line data    Source code
       1           43 : import React from 'react';
       2              : 
       3              : import { Badge } from "@patternfly/react-core/dist/esm/components/Badge";
       4              : import { Button } from "@patternfly/react-core/dist/esm/components/Button";
       5              : import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card";
       6              : import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content";
       7              : import { Divider } from "@patternfly/react-core/dist/esm/components/Divider";
       8              : import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
       9              : import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
      10              : import { LabelGroup } from "@patternfly/react-core/dist/esm/components/Label";
      11              : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
      12              : import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar";
      13              : import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";
      14              : import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex";
      15              : import { MicrochipIcon, MemoryIcon, PortIcon, VolumeIcon, } from '@patternfly/react-icons';
      16              : import { cellWidth, SortByDirection } from '@patternfly/react-table';
      17              : import { KebabDropdown } from "cockpit-components-dropdown.jsx";
      18              : import { useDialogs, DialogsContext } from "dialogs.jsx";
      19              : 
      20              : import cockpit from 'cockpit';
      21              : import { ListingPanel } from 'cockpit-components-listing-panel';
      22              : import { ListingTable } from "cockpit-components-table";
      23              : import * as machine_info from 'machine-info';
      24              : 
      25              : import ContainerCheckpointModal from './ContainerCheckpointModal.jsx';
      26              : import ContainerCommitModal from './ContainerCommitModal.jsx';
      27              : import ContainerDeleteModal from './ContainerDeleteModal.jsx';
      28              : import ContainerDetails from './ContainerDetails.jsx';
      29              : import ContainerHealthLogs from './ContainerHealthLogs.jsx';
      30              : import ContainerIntegration, { renderContainerPublishedPorts, renderContainerVolumes } from './ContainerIntegration.jsx';
      31              : import ContainerLogs from './ContainerLogs.jsx';
      32              : import ContainerRenameModal from './ContainerRenameModal.jsx';
      33              : import ContainerRestoreModal from './ContainerRestoreModal.jsx';
      34              : import ContainerTerminal from './ContainerTerminal.jsx';
      35              : import ForceRemoveModal from './ForceRemoveModal.jsx';
      36              : import { ImageRunModal } from './ImageRunModal.jsx';
      37              : import { PodActions } from './PodActions.jsx';
      38              : import { PodCreateModal } from './PodCreateModal.jsx';
      39              : import PruneUnusedContainersModal from './PruneUnusedContainersModal.jsx';
      40              : import * as client from './client.js';
      41              : import * as utils from './util.js';
      42              : 
      43              : import './Containers.scss';
      44              : import '@patternfly/patternfly/utilities/Accessibility/accessibility.css';
      45              : 
      46           43 : const _ = cockpit.gettext;
      47              : 
      48           41 : const ContainerActions = ({ con, container, onAddNotification, localImages, updateContainer, isSystemdService, isDownloading }) => {
      49           41 :     const Dialogs = useDialogs();
      50           41 :     const isRunning = container.State.Status == "running";
      51           41 :     const isPaused = container.State.Status === "paused";
      52              : 
      53            1 :     const deleteContainer = () => {
      54            1 :         if (container.State.Status == "running") {
      55            1 :             const handleForceRemoveContainer = () => {
      56            0 :                 const id = container ? container.Id : "";
      57              : 
      58            1 :                 return client.delContainer(con, id, true)
      59            0 :                         .catch(ex => {
      60            0 :                             const error = cockpit.format(_("Failed to force remove container $0"), container.Name); // not-covered: OS error
      61            0 :                             onAddNotification({ type: 'danger', error, errorDetail: ex.message });
      62            0 :                         })
      63            1 :                         .finally(() => {
      64            1 :                             Dialogs.close();
      65            1 :                         });
      66            1 :             };
      67              : 
      68            1 :             Dialogs.show(<ForceRemoveModal name={container.Name}
      69            1 :                                            handleForceRemove={handleForceRemoveContainer}
      70            1 :                                            reason={_("Deleting a running container will erase all data in it.")} />);
      71            1 :         } else {
      72            1 :             Dialogs.show(<ContainerDeleteModal con={con}
      73            1 :                                                containerWillDelete={container}
      74            1 :                                                onAddNotification={onAddNotification} />);
      75            1 :         }
      76            1 :     };
      77              : 
      78            3 :     const stopContainer = (force) => {
      79            3 :         const args = {};
      80              : 
      81            3 :         if (force)
      82            3 :             args.t = 0;
      83            3 :         client.postContainer(con, "stop", container.Id, args)
      84            0 :                 .catch(ex => {
      85            0 :                     const error = cockpit.format(_("Failed to stop container $0"), container.Name); // not-covered: OS error
      86            0 :                     onAddNotification({ type: 'danger', error, errorDetail: ex.message });
      87            0 :                 });
      88            3 :     };
      89              : 
      90            4 :     const startContainer = () => {
      91            4 :         client.postContainer(con, "start", container.Id, {})
      92            0 :                 .catch(ex => {
      93            0 :                     const error = cockpit.format(_("Failed to start container $0"), container.Name); // not-covered: OS error
      94            0 :                     onAddNotification({ type: 'danger', error, errorDetail: ex.message });
      95            0 :                 });
      96            4 :     };
      97              : 
      98            2 :     const resumeContainer = () => {
      99            2 :         client.postContainer(con, "unpause", container.Id, {})
     100            0 :                 .catch(ex => {
     101            0 :                     const error = cockpit.format(_("Failed to resume container $0"), container.Name); // not-covered: OS error
     102            0 :                     onAddNotification({ type: 'danger', error, errorDetail: ex.message });
     103            0 :                 });
     104            2 :     };
     105              : 
     106            2 :     const pauseContainer = () => {
     107            2 :         client.postContainer(con, "pause", container.Id, {})
     108            0 :                 .catch(ex => {
     109            0 :                     const error = cockpit.format(_("Failed to pause container $0"), container.Name); // not-covered: OS error
     110            0 :                     onAddNotification({ type: 'danger', error, errorDetail: ex.message });
     111            0 :                 });
     112            2 :     };
     113              : 
     114            2 :     const commitContainer = () => {
     115            2 :         Dialogs.show(<ContainerCommitModal con={con}
     116            2 :                                            container={container}
     117            2 :                                            localImages={localImages} />);
     118            2 :     };
     119              : 
     120            2 :     const restartContainer = (force) => {
     121            2 :         const args = {};
     122              : 
     123            2 :         if (force)
     124            2 :             args.t = 0;
     125            2 :         client.postContainer(con, "restart", container.Id, args)
     126            0 :                 .catch(ex => {
     127            0 :                     const error = cockpit.format(_("Failed to restart container $0"), container.Name); // not-covered: OS error
     128            0 :                     onAddNotification({ type: 'danger', error, errorDetail: ex.message });
     129            0 :                 });
     130            2 :     };
     131              : 
     132            2 :     const renameContainer = () => {
     133            2 :         if (container.State.Status !== "running") {
     134            2 :             Dialogs.show(<ContainerRenameModal con={con}
     135            2 :                                                container={container}
     136            2 :                                                updateContainer={updateContainer} />);
     137            2 :         }
     138            2 :     };
     139              : 
     140            1 :     const checkpointContainer = () => {
     141            1 :         Dialogs.show(<ContainerCheckpointModal con={con}
     142            1 :                                                containerWillCheckpoint={container}
     143            1 :                                                onAddNotification={onAddNotification} />);
     144            1 :     };
     145              : 
     146            1 :     const restoreContainer = () => {
     147            1 :         Dialogs.show(<ContainerRestoreModal con={con}
     148            1 :                                             containerWillRestore={container}
     149            1 :                                             onAddNotification={onAddNotification} />);
     150            1 :     };
     151              : 
     152           38 :     const addRenameAction = () => {
     153           38 :         actions.push(
     154           38 :             <DropdownItem key="rename"
     155            2 :                         onClick={() => renameContainer()}>
     156           38 :                 {_("Rename")}
     157           38 :             </DropdownItem>
     158           38 :         );
     159           38 :     };
     160              : 
     161           41 :     const actions = [];
     162           23 :     if (isRunning || isPaused) {
     163              :         // TODO: cockpit-podman currently isn't aware of podman quadlets as they don't keep containers around when stopped, failed or not yet started by default.
     164              :         // Allowing a user to stop or restart a container can result in the container disappearing from the list without a way to start it again. Until cockpit-podman
     165              :         // can list quadlets these actions are disabled.
     166           30 :         if (!isSystemdService) {
     167           30 :             actions.push(
     168           30 :                 <DropdownItem key="stop"
     169            2 :                           onClick={() => stopContainer()}>
     170           30 :                     {_("Stop")}
     171           30 :                 </DropdownItem>,
     172           30 :                 <DropdownItem key="force-stop"
     173            3 :                           onClick={() => stopContainer(true)}>
     174           30 :                     {_("Force stop")}
     175           30 :                 </DropdownItem>,
     176           30 :                 <DropdownItem key="restart"
     177            0 :                           onClick={() => restartContainer()}>
     178           30 :                     {_("Restart")}
     179           30 :                 </DropdownItem>,
     180           30 :                 <DropdownItem key="force-restart"
     181            2 :                           onClick={() => restartContainer(true)}>
     182           30 :                     {_("Force restart")}
     183           30 :                 </DropdownItem>
     184           30 :             );
     185              : 
     186           30 :             if (!isPaused) {
     187           30 :                 actions.push(
     188           30 :                     <DropdownItem key="pause"
     189            2 :                           onClick={() => pauseContainer()}>
     190           30 :                         {_("Pause")}
     191           30 :                     </DropdownItem>
     192           30 :                 );
     193            3 :             } else {
     194            3 :                 actions.push(
     195            3 :                     <DropdownItem key="resume"
     196            2 :                           onClick={() => resumeContainer()}>
     197            3 :                         {_("Resume")}
     198            3 :                     </DropdownItem>
     199            3 :                 );
     200            3 :             }
     201           30 :         }
     202              : 
     203           21 :         if (container.uid == 0 && !isPaused) {
     204           21 :             if (actions.length > 0)
     205           18 :                 actions.push(<Divider key="separator-0" />);
     206              : 
     207           21 :             actions.push(
     208           21 :                 <DropdownItem key="checkpoint"
     209            1 :                               onClick={() => checkpointContainer()}>
     210           21 :                     {_("Checkpoint")}
     211           21 :                 </DropdownItem>
     212           21 :             );
     213           21 :         }
     214           36 :     }
     215              : 
     216           26 :     if (!isRunning && !isPaused) {
     217           26 :         actions.push(
     218           26 :             <DropdownItem key="start"
     219            4 :                           onClick={() => startContainer()}>
     220           26 :                 {_("Start")}
     221           26 :             </DropdownItem>
     222           26 :         );
     223           26 :         if (!isSystemdService) {
     224           26 :             addRenameAction();
     225           26 :         }
     226            1 :         if (container.uid == 0 && container.State?.CheckpointPath) {
     227            1 :             actions.push(
     228            1 :                 <Divider key="separator-0" />,
     229            1 :                 <DropdownItem key="restore"
     230            1 :                               onClick={() => restoreContainer()}>
     231            1 :                     {_("Restore")}
     232            1 :                 </DropdownItem>
     233            1 :             );
     234            1 :         }
     235           21 :     } else { // running or paused
     236           30 :         if (!isSystemdService) {
     237           30 :             addRenameAction();
     238           30 :         }
     239           36 :     }
     240              : 
     241           41 :     actions.push(<Divider key="separator-1" />);
     242           41 :     actions.push(
     243           41 :         <DropdownItem key="commit"
     244            2 :                       onClick={() => commitContainer()}>
     245           41 :             {_("Commit")}
     246           41 :         </DropdownItem>
     247           41 :     );
     248              : 
     249           38 :     if (!isSystemdService) {
     250           38 :         actions.push(<Divider key="separator-2" />);
     251           38 :         actions.push(
     252           38 :             <DropdownItem key="delete"
     253           38 :                       className="pf-m-danger"
     254           38 :                       onClick={deleteContainer}>
     255           38 :                 {_("Delete")}
     256           38 :             </DropdownItem>
     257           38 :         );
     258           38 :     }
     259              : 
     260           41 :     return <KebabDropdown position="right" dropdownItems={actions} isDisabled={isDownloading} />;
     261           41 : };
     262              : 
     263            3 : export let onDownloadContainer = function funcOnDownloadContainer(container) {
     264            3 :     this.setState(prevState => ({
     265            3 :         downloadingContainers: [...prevState.downloadingContainers, container]
     266            3 :     }));
     267            3 : };
     268              : 
     269            2 : export let onDownloadContainerFinished = function funcOnDownloadContainerFinished(container) {
     270            2 :     this.setState(prevState => ({
     271            2 :         downloadingContainers: prevState.downloadingContainers.filter(entry => entry.name !== container.name),
     272            2 :     }));
     273            2 : };
     274              : 
     275            2 : const localize_health = (state) => {
     276            2 :     if (state === "healthy")
     277            2 :         return _("Healthy");
     278            2 :     else if (state === "unhealthy")
     279            2 :         return _("Unhealthy");
     280            2 :     else if (state === "starting")
     281            0 :         return _("Checking health");
     282              :     else
     283            0 :         console.error("Unexpected health check status", state);
     284            0 :     return null;
     285            2 : };
     286              : 
     287           43 : const ContainerOverActions = ({ handlePruneUnusedContainers, unusedContainers }) => {
     288           43 :     const actions = [
     289           43 :         <DropdownItem key="prune-unused-containers"
     290           43 :                             id="prune-unused-containers-button"
     291           43 :                             component="button"
     292           43 :                             className="pf-m-danger btn-delete"
     293            2 :                             onClick={() => handlePruneUnusedContainers()}
     294           43 :                             isDisabled={unusedContainers.length === 0}>
     295           43 :             {_("Prune unused containers")}
     296           43 :         </DropdownItem>,
     297           43 :     ];
     298              : 
     299           43 :     return <KebabDropdown toggleButtonId="containers-actions-dropdown" position="right" dropdownItems={actions} />;
     300           43 : };
     301              : 
     302           43 : class Containers extends React.Component {
     303           43 :     static contextType = DialogsContext;
     304              : 
     305           43 :     constructor(props) {
     306           43 :         super(props);
     307           43 :         this.state = {
     308           43 :             width: 0,
     309           43 :             memTotal: 0,
     310           43 :             downloadingContainers: [],
     311           43 :             showPruneUnusedContainersModal: false,
     312           43 :         };
     313           43 :         this.renderRow = this.renderRow.bind(this);
     314           43 :         this.onWindowResize = this.onWindowResize.bind(this);
     315           43 :         this.podStats = this.podStats.bind(this);
     316              : 
     317           43 :         this.cardRef = React.createRef();
     318              : 
     319           43 :         onDownloadContainer = onDownloadContainer.bind(this);
     320           43 :         onDownloadContainerFinished = onDownloadContainerFinished.bind(this);
     321              : 
     322           43 :         machine_info.cpu_ram_info()
     323           43 :                 .then(info => this.setState({ memTotal: info.memory }));
     324              : 
     325           43 :         window.addEventListener('resize', this.onWindowResize);
     326           43 :     }
     327              : 
     328           43 :     componentDidMount() {
     329           43 :         this.onWindowResize();
     330           43 :     }
     331              : 
     332            1 :     componentWillUnmount() {
     333            1 :         window.removeEventListener('resize', this.onWindowResize);
     334            1 :     }
     335              : 
     336           41 :     renderRow(containersStats, container, localImages) {
     337           41 :         const containerStats = containersStats[container.key];
     338           41 :         const image = container.ImageName;
     339           21 :         const isToolboxContainer = container.Config?.Labels?.["com.github.containers.toolbox"] === "true";
     340           21 :         const isDistroboxContainer = container.Config?.Labels?.manager === "distrobox";
     341           21 :         const isSystemdService = Boolean(container.Config?.Labels?.PODMAN_SYSTEMD_UNIT);
     342           41 :         let localized_health = null;
     343              : 
     344              :         // this needs to get along with stub containers from image run dialog, where most properties don't exist yet
     345              :         // HACK: Podman renamed `Healthcheck` to `Health` randomly
     346              :         // https://github.com/containers/podman/commit/119973375
     347            0 :         const healthcheck = container.State?.Health?.Status ?? container.State?.Healthcheck?.Status; // not-covered: only on old version
     348            0 :         const status = container.State?.Status ?? ""; // not-covered: race condition
     349              : 
     350           41 :         let proc = "";
     351           41 :         let mem = "";
     352            0 :         if (this.props.cgroupVersion == 'v1' && container.uid !== 0 && status == 'running') { // not-covered: only on old version
     353            0 :             proc = <div><abbr title={_("not available")}>{_("n/a")}</abbr></div>;
     354            0 :             mem = <div><abbr title={_("not available")}>{_("n/a")}</abbr></div>;
     355            0 :         }
     356           24 :         if (containerStats && status === "running") {
     357              :             // container.HostConfig.Memory (0 by default), containerStats.MemUsage
     358           24 :             if (containerStats.CPU != undefined)
     359           24 :                 proc = <div className="ct-numeric-column">{containerStats.CPU.toFixed(2) + "%"}</div>;
     360           24 :             if (Number.isInteger(containerStats.MemUsage) && this.state.memTotal) {
     361              :                 // the primary view is how much of the host's memory a container uses, for comparability
     362           24 :                 const mem_pct = Math.round(containerStats.MemUsage / this.state.memTotal * 100);
     363           24 :                 const mem_items = [
     364           24 :                     <span key="pct">{cockpit.format("$0%", mem_pct)}</span>,
     365           24 :                     <small key="abs">{cockpit.format_bytes(containerStats.MemUsage)}</small>
     366           24 :                 ];
     367              : 
     368              :                 // is there a configured limit?
     369            4 :                 if (container.HostConfig?.Memory) {
     370            4 :                     const limit_pct = Math.round(containerStats.MemUsage / container.HostConfig.Memory * 100);
     371            4 :                     mem_items.push(
     372            4 :                         <small key="limit">
     373            4 :                             { cockpit.format(
     374            4 :                                 _("$0% of $1 limit"),
     375            4 :                                 limit_pct,
     376            4 :                                 cockpit.format_bytes(container.HostConfig.Memory)) }
     377            4 :                         </small>
     378            4 :                     );
     379            4 :                 }
     380              : 
     381           24 :                 mem = <div className="container-block ct-numeric-column">{mem_items}</div>;
     382           24 :             }
     383           24 :         }
     384           41 :         const info_block = (
     385           41 :             <div className="container-block">
     386           41 :                 <Flex alignItems={{ default: 'alignItemsCenter' }}>
     387           41 :                     <span className="container-name">{container.Name}</span>
     388            1 :                     {isToolboxContainer && <Badge className='ct-badge-toolbox'>toolbox</Badge>}
     389            1 :                     {isDistroboxContainer && <Badge className='ct-badge-distrobox'>distrobox</Badge>}
     390            6 :                     {isSystemdService && <Badge className='ct-badge-service'>{_("service")}</Badge>}
     391           41 :                 </Flex>
     392           41 :                 <small>{image}</small>
     393           41 :                 <small>{utils.quote_cmdline(container.Config?.Cmd)}</small>
     394           41 :             </div>
     395              :         );
     396              : 
     397           41 :         let containerStateClass = "ct-badge-container-" + status.toLowerCase();
     398           41 :         if (container.isDownloading)
     399            3 :             containerStateClass += " downloading";
     400              : 
     401           41 :         const containerState = status.charAt(0).toUpperCase() + status.slice(1);
     402              : 
     403           41 :         const state = [<Badge key={containerState} isRead className={containerStateClass}>{_(containerState)}</Badge>]; // States are defined in util.js
     404            2 :         if (healthcheck) {
     405            2 :             localized_health = localize_health(healthcheck);
     406            2 :             if (localized_health)
     407            2 :                 state.push(<Badge key={healthcheck} isRead className={"ct-badge-container-" + healthcheck}>{localized_health}</Badge>);
     408            2 :         }
     409              : 
     410           41 :         const user = this.props.users.find(user => user.uid === container.uid);
     411           41 :         cockpit.assert(user, `User not found for container uid ${container.uid}`);
     412              : 
     413           41 :         const columns = [
     414            0 :             { title: info_block, sortKey: container.Name ?? container.Id },
     415           41 :             {
     416            4 :                 title: (container.uid === 0) ? _("system") : <div><span className="ct-grey-text">{_("user:")} </span>{user.name}</div>,
     417           41 :                 props: { modifier: "nowrap" },
     418           41 :                 sortKey: container.key,
     419           41 :             },
     420           17 :             { title: proc, props: { modifier: "nowrap" }, sortKey: containerState === "Running" ? containerStats?.CPU ?? -1 : -1 },
     421           24 :             { title: mem, props: { modifier: "nowrap" }, sortKey: containerStats?.MemUsage ?? -1 },
     422           41 :             { title: <LabelGroup isVertical>{state}</LabelGroup>, sortKey: containerState },
     423           41 :         ];
     424              : 
     425           41 :         columns.push({
     426           41 :             title: <ContainerActions con={user.con}
     427           41 :                                      container={container}
     428           41 :                                      onAddNotification={this.props.onAddNotification}
     429           41 :                                      localImages={localImages}
     430           41 :                                      updateContainer={this.props.updateContainer}
     431           41 :                                      isSystemdService={isSystemdService}
     432           41 :                                      isDownloading={container.isDownloading} />,
     433           41 :             props: { className: "pf-v6-c-table__action" }
     434           41 :         });
     435              : 
     436           41 :         const tty = !!container.Config?.Tty;
     437              : 
     438           41 :         const tabs = [];
     439           41 :         if (container.State && user.con !== null) {
     440           41 :             tabs.push({
     441           41 :                 name: _("Details"),
     442           41 :                 renderer: ContainerDetails,
     443           41 :                 data: { container }
     444           41 :             });
     445              : 
     446           41 :             if (!container.isDownloading) {
     447           41 :                 tabs.push({
     448           41 :                     name: _("Integration"),
     449           41 :                     renderer: ContainerIntegration,
     450           41 :                     data: { container, localImages }
     451           41 :                 });
     452           41 :                 tabs.push({
     453           41 :                     name: _("Logs"),
     454           41 :                     renderer: ContainerLogs,
     455           41 :                     data: {
     456           41 :                         containerId: container.Id,
     457           41 :                         containerStatus: container.State.Status,
     458           41 :                         width: this.state.width,
     459           41 :                         uid: container.uid,
     460           21 :                         systemd_unit: container.Config?.Labels?.PODMAN_SYSTEMD_UNIT,
     461           41 :                     }
     462           41 :                 });
     463           41 :                 tabs.push({
     464           41 :                     name: _("Console"),
     465           41 :                     renderer: ContainerTerminal,
     466           41 :                     data: { con: user.con, containerId: container.Id, containerStatus: container.State.Status, width: this.state.width, uid: container.uid, tty }
     467           41 :                 });
     468           41 :             }
     469           41 :         }
     470              : 
     471            2 :         if (healthcheck) {
     472            2 :             tabs.push({
     473            2 :                 name: _("Health check"),
     474            2 :                 renderer: ContainerHealthLogs,
     475            2 :                 data: { con: user.con, container, onAddNotification: this.props.onAddNotification, state: localized_health }
     476            2 :             });
     477            2 :         }
     478              : 
     479           41 :         return {
     480           41 :             expandedContent: <ListingPanel colSpan='4' tabRenderers={tabs} />,
     481           41 :             columns,
     482           41 :             initiallyExpanded: document.location.hash.substring(1) === container.Id,
     483           41 :             props: {
     484           41 :                 key: container.key,
     485           41 :                 "data-row-id": container.key,
     486           41 :                 "data-started-at": container.State?.StartedAt,
     487           41 :             },
     488           41 :         };
     489           41 :     }
     490              : 
     491           43 :     onWindowResize() {
     492           43 :         this.setState({ width: this.cardRef.current.clientWidth });
     493           43 :     }
     494              : 
     495           10 :     podStats(pod) {
     496           10 :         const { containersStats } = this.props;
     497              :         // when no containers exists pod.Containers is null
     498            0 :         if (!containersStats || !pod.Containers) {
     499            0 :             return null;
     500            0 :         }
     501              : 
     502              :         // As podman does not provide per pod memory/cpu statistics we do the following:
     503              :         // - add up CPU usage to display total CPU use of all containers in the pod
     504              :         // - add up memory usage so it displays the total memory of the pod.
     505           10 :         let cpu = 0;
     506           10 :         let mem = 0;
     507            8 :         for (const container of pod.Containers) {
     508            8 :             const containerStats = containersStats[utils.makeKey(pod.uid, container.Id)];
     509            8 :             if (!containerStats)
     510            8 :                 continue;
     511              : 
     512            4 :             if (containerStats.CPU != undefined) {
     513            4 :                 cpu += containerStats.CPU;
     514            4 :             }
     515            4 :             if (containerStats.MemUsage != undefined) {
     516            4 :                 mem += containerStats.MemUsage;
     517            4 :             }
     518            8 :         }
     519              : 
     520           10 :         return {
     521           10 :             cpu: cpu.toFixed(2),
     522           10 :             mem,
     523           10 :         };
     524           10 :     }
     525              : 
     526           10 :     renderPodDetails(pod, podStatus) {
     527           10 :         const podStats = this.podStats(pod);
     528           10 :         const infraContainer = this.props.containers[utils.makeKey(pod.uid, pod.InfraId)];
     529            4 :         const numPorts = Object.keys(infraContainer?.NetworkSettings?.Ports ?? {}).length;
     530              : 
     531           10 :         return (
     532           10 :             <>
     533           10 :                 {podStats && podStatus === "Running" &&
     534            6 :                     <>
     535            6 :                         <Flex className='pod-stat' spaceItems={{ default: 'spaceItemsSm' }}>
     536            6 :                             <Tooltip content={_("CPU")}>
     537            6 :                                 <MicrochipIcon />
     538            6 :                             </Tooltip>
     539            6 :                             <Content component={ContentVariants.p} className="pf-v6-u-hidden-on-sm">{_("CPU")}</Content>
     540            6 :                             <Content component={ContentVariants.p} className="pod-cpu">{podStats.cpu}%</Content>
     541            6 :                         </Flex>
     542            6 :                         <Flex className='pod-stat' spaceItems={{ default: 'spaceItemsSm' }}>
     543            6 :                             <Tooltip content={_("Memory")}>
     544            6 :                                 <MemoryIcon />
     545            6 :                             </Tooltip>
     546            6 :                             <Content component={ContentVariants.p} className="pf-v6-u-hidden-on-sm">{_("Memory")}</Content>
     547            6 :                             <Content component={ContentVariants.p} className="pod-memory">{cockpit.format_bytes(podStats.mem)}</Content>
     548            6 :                         </Flex>
     549            6 :                     </>
     550              :                 }
     551           10 :                 {infraContainer &&
     552            7 :                 <>
     553            7 :                     {numPorts > 0 &&
     554            3 :                         <Tooltip content={_("Click to see published ports")}>
     555            3 :                             <Popover
     556            3 :                               enableFlip
     557            3 :                               bodyContent={renderContainerPublishedPorts(infraContainer.NetworkSettings.Ports)}
     558              :                             >
     559            3 :                                 <Button size="sm" variant="link" className="pod-details-button pod-details-ports-btn"
     560            3 :                                         icon={<PortIcon className="pod-details-button-color" />}
     561              :                                 >
     562            3 :                                     {numPorts}
     563            3 :                                     <Content component={ContentVariants.p} className="pf-v6-u-hidden-on-sm">{_("ports")}</Content>
     564            3 :                                 </Button>
     565            3 :                             </Popover>
     566            3 :                         </Tooltip>
     567              :                     }
     568            7 :                     {infraContainer.Mounts && infraContainer.Mounts.length !== 0 &&
     569            3 :                     <Tooltip content={_("Click to see volumes")}>
     570            3 :                         <Popover
     571            3 :                       enableFlip
     572            3 :                       bodyContent={renderContainerVolumes(infraContainer.Mounts)}
     573              :                         >
     574            3 :                             <Button size="sm" variant="link" className="pod-details-button pod-details-volumes-btn"
     575            3 :                             icon={<VolumeIcon className="pod-details-button-color" />}
     576              :                             >
     577            3 :                                 {infraContainer.Mounts.length}
     578            3 :                                 <Content component={ContentVariants.p} className="pf-v6-u-hidden-on-sm">{_("volumes")}</Content>
     579            3 :                             </Button>
     580            3 :                         </Popover>
     581            3 :                     </Tooltip>
     582              :                     }
     583            7 :                 </>
     584              :                 }
     585           10 :             </>
     586              :         );
     587           10 :     }
     588              : 
     589            2 :     onOpenPruneUnusedContainersDialog = () => {
     590            2 :         this.setState({ showPruneUnusedContainersModal: true });
     591            2 :     };
     592              : 
     593           43 :     render() {
     594           43 :         const Dialogs = this.context;
     595           43 :         const columnTitles = [
     596           43 :             { title: _("Container"), transforms: [cellWidth(20)], sortable: true },
     597           43 :             { title: _("Owner"), sortable: true },
     598           43 :             { title: _("CPU"), sortable: true, props: { className: 'ct-numeric-column' } },
     599           43 :             { title: _("Memory"), sortable: true, props: { className: 'ct-numeric-column' } },
     600           43 :             { title: _("State"), sortable: true },
     601           43 :             { title: "", sortable: false, props: { screenReaderText: _("Actions") } },
     602           43 :         ];
     603           43 :         const partitionedContainers = { 'no-pod': [] };
     604           43 :         let filtered = [];
     605           43 :         const unusedContainers = [];
     606              : 
     607           43 :         let emptyCaption = _("No containers");
     608           43 :         const emptyCaptionPod = _("No containers in this pod");
     609           43 :         if (this.props.containers === null || this.props.pods === null)
     610           43 :             emptyCaption = _("Loading...");
     611           43 :         else if (this.props.textFilter.length > 0)
     612            3 :             emptyCaption = _("No containers that match the current filter");
     613           43 :         else if (this.props.filter == "running")
     614            4 :             emptyCaption = _("No running containers");
     615              : 
     616           43 :         if (this.props.containers !== null && this.props.pods !== null) {
     617            4 :             filtered = Object.keys(this.props.containers).filter(id => !(this.props.filter == "running") || ["running", "restarting"].includes(this.props.containers[id].State.Status));
     618              : 
     619            1 :             if (this.props.ownerFilter !== "all") {
     620            1 :                 filtered = filtered.filter(id => {
     621            1 :                     if (this.props.ownerFilter === "user")
     622            0 :                         return this.props.containers[id].uid === null;
     623            1 :                     return this.props.containers[id].uid === this.props.ownerFilter;
     624            1 :                 });
     625            1 :             }
     626              : 
     627            3 :             if (this.props.textFilter.length > 0) {
     628            3 :                 const lcf = this.props.textFilter.toLowerCase();
     629            3 :                 filtered = filtered.filter(id => this.props.containers[id].Name.toLowerCase().indexOf(lcf) >= 0 ||
     630            2 :                     (this.props.containers[id].Pod &&
     631            0 :                      this.props.pods[utils.makeKey(this.props.containers[id].uid, this.props.containers[id].Pod)].Name.toLowerCase().indexOf(lcf) >= 0) ||
     632            2 :                     this.props.containers[id].ImageName.toLowerCase().indexOf(lcf) >= 0
     633            3 :                 );
     634            3 :             }
     635              : 
     636              :             // Remove infra containers
     637           41 :             filtered = filtered.filter(id => !this.props.containers[id].IsInfra);
     638              : 
     639           28 :             const getHealth = id => {
     640           28 :                 const state = this.props.containers[id]?.State;
     641            0 :                 return state?.Health?.Status || state?.Healthcheck?.Status;
     642           28 :             };
     643              : 
     644           28 :             filtered.sort((a, b) => {
     645              :                 // Show unhealthy containers first
     646           28 :                 const a_health = getHealth(a);
     647           28 :                 const b_health = getHealth(b);
     648            2 :                 if (a_health !== b_health) {
     649            2 :                     if (a_health === "unhealthy")
     650            2 :                         return -1;
     651            2 :                     if (b_health === "unhealthy")
     652            2 :                         return 1;
     653            2 :                 }
     654              :                 // User containers are in front of system ones
     655           28 :                 if (this.props.containers[a].uid !== this.props.containers[b].uid)
     656            0 :                     return (this.props.containers[a].uid === 0) ? 1 : -1;
     657           10 :                 return this.props.containers[a].Name > this.props.containers[b].Name ? 1 : -1;
     658           28 :             });
     659              : 
     660            0 :             Object.keys(this.props.pods || {}).forEach(pod => { partitionedContainers[pod] = [] });
     661              : 
     662           41 :             filtered.forEach(id => {
     663           41 :                 const container = this.props.containers[id];
     664           41 :                 if (container)
     665            2 :                     (partitionedContainers[container.Pod ? utils.makeKey(container.uid, container.Pod) : 'no-pod'] || []).push(container);
     666           41 :             });
     667              : 
     668              :             // Append downloading containers
     669            3 :             this.state.downloadingContainers.forEach(cont => {
     670            3 :                 partitionedContainers['no-pod'].push(cont);
     671            3 :             });
     672              : 
     673              :             // Apply filters to pods
     674           43 :             Object.keys(partitionedContainers).forEach(section => {
     675           43 :                 const lcf = this.props.textFilter.toLowerCase();
     676           10 :                 if (section != "no-pod") {
     677           10 :                     const pod = this.props.pods[section];
     678            1 :                     if ((this.props.filter == "running" && pod.Status != "Running") ||
     679              :                         // If nor the pod name nor any container inside the pod fit the filter, hide the whole pod
     680            8 :                         (!partitionedContainers[section].length && pod.Name.toLowerCase().indexOf(lcf) < 0) ||
     681           10 :                         (this.props.ownerFilter !== "all" &&
     682            0 :                          ((this.props.ownerFilter === "user" && pod.uid !== null) ||
     683            0 :                             (this.props.ownerFilter !== "user" && pod.uid !== this.props.ownerFilter))))
     684            1 :                         delete partitionedContainers[section];
     685           10 :                 }
     686           43 :             });
     687              :             // If there are pods to show and the generic container list is empty don't show  it at all
     688           10 :             if (Object.keys(partitionedContainers).length > 1 && !partitionedContainers["no-pod"].length)
     689            7 :                 delete partitionedContainers["no-pod"];
     690              : 
     691           43 :             const prune_states = ["created", "configured", "stopped", "exited"];
     692           41 :             for (const containerid of Object.keys(this.props.containers)) {
     693           41 :                 const container = this.props.containers[containerid];
     694              :                 // Ignore pods and running containers
     695           26 :                 if (!prune_states.includes(container.State.Status) || container.Pod)
     696           41 :                     continue;
     697              : 
     698           23 :                 unusedContainers.push({
     699           23 :                     id: container.Id,
     700           23 :                     name: container.Name,
     701           23 :                     key: container.key,
     702           23 :                     created: container.Created,
     703           23 :                     uid: container.uid,
     704           23 :                 });
     705           23 :             }
     706           43 :         }
     707              : 
     708              :         // Convert to the search result output
     709           43 :         let localImages = null;
     710           43 :         let nonIntermediateImages = null;
     711           43 :         if (this.props.images) {
     712           43 :             localImages = Object.keys(this.props.images).map(id => {
     713           43 :                 const img = this.props.images[id];
     714            4 :                 img.Index = img.RepoTags?.[0] ? img.RepoTags[0].split('/')[0] : "";
     715           43 :                 img.Name = utils.image_name(img);
     716            7 :                 img.toString = function imgToString() { return this.Name };
     717           43 :                 return img;
     718           43 :             }, []);
     719           43 :             nonIntermediateImages = localImages.filter(img => img.Index !== "");
     720           43 :         }
     721              : 
     722            7 :         const createContainer = (inPod) => {
     723            7 :             if (nonIntermediateImages)
     724            7 :                 Dialogs.show(
     725            7 :                     <utils.PodmanInfoContext.Consumer>
     726            7 :                         {(podmanInfo) => (
     727            7 :                             <DialogsContext.Consumer>
     728            7 :                                 {(Dialogs) => (
     729            7 :                                     <ImageRunModal users={this.props.users}
     730            7 :                                                    localImages={nonIntermediateImages}
     731            7 :                                                    pod={inPod}
     732            7 :                                                    onAddNotification={this.props.onAddNotification}
     733            7 :                                                    podmanInfo={podmanInfo}
     734            7 :                                                    dialogs={Dialogs} />
     735              :                                 )}
     736            7 :                             </DialogsContext.Consumer>
     737              :                         )}
     738            7 :                     </utils.PodmanInfoContext.Consumer>);
     739            7 :         };
     740              : 
     741            2 :         const createPod = () => {
     742            2 :             Dialogs.show(<PodCreateModal
     743            2 :                 users={this.props.users}
     744            2 :                 onAddNotification={this.props.onAddNotification} />);
     745            2 :         };
     746              : 
     747           43 :         const filterRunning = (
     748           43 :             <Toolbar>
     749           43 :                 <ToolbarContent className="containers-containers-toolbarcontent">
     750           43 :                     <ToolbarItem alignSelf="center" variant="label" htmlFor="containers-containers-filter">
     751           43 :                         {_("Show")}
     752           43 :                     </ToolbarItem>
     753           43 :                     <ToolbarItem>
     754           17 :                         <FormSelect id="containers-containers-filter" value={this.props.filter} onChange={(_, value) => this.props.handleFilterChange(value)}>
     755           43 :                             <FormSelectOption value='all' label={_("All")} />
     756           43 :                             <FormSelectOption value='running' label={_("Only running")} />
     757           43 :                         </FormSelect>
     758           43 :                     </ToolbarItem>
     759           43 :                     <Divider orientation={{ default: "vertical" }} />
     760           43 :                     <ToolbarItem>
     761           43 :                         <Button variant="secondary" key="create-new-pod-action"
     762           43 :                                 id="containers-containers-create-pod-btn"
     763            2 :                                 onClick={() => createPod()}>
     764           43 :                             {_("Create pod")}
     765           43 :                         </Button>
     766           43 :                     </ToolbarItem>
     767           43 :                     <ToolbarItem>
     768           43 :                         <Button variant="primary" key="get-new-image-action"
     769           43 :                                 id="containers-containers-create-container-btn"
     770           43 :                                 isDisabled={nonIntermediateImages === null}
     771            5 :                                 onClick={() => createContainer(null)}>
     772           43 :                             {_("Create container")}
     773           43 :                         </Button>
     774           43 :                     </ToolbarItem>
     775           43 :                     <ToolbarItem>
     776           43 :                         <ContainerOverActions unusedContainers={unusedContainers} handlePruneUnusedContainers={this.onOpenPruneUnusedContainersDialog} />
     777           43 :                     </ToolbarItem>
     778           43 :                 </ToolbarContent>
     779           43 :             </Toolbar>
     780              :         );
     781              : 
     782           41 :         const sortRows = (rows, direction, idx) => {
     783              :             // CPU / Memory /States
     784           41 :             const isNumeric = idx == 2 || idx == 3 || idx == 4;
     785           41 :             const stateOrderMapping = {};
     786           41 :             utils.states.forEach((elem, index) => {
     787           41 :                 stateOrderMapping[elem] = index;
     788           41 :             });
     789           26 :             const sortedRows = rows.sort((a, b) => {
     790            0 :                 let aitem = a.columns[idx].sortKey ?? a.columns[idx].title;
     791            0 :                 let bitem = b.columns[idx].sortKey ?? b.columns[idx].title;
     792              :                 // Sort the states based on the order defined in utils. so Running first.
     793            0 :                 if (idx === 4) {
     794            0 :                     aitem = stateOrderMapping[aitem];
     795            0 :                     bitem = stateOrderMapping[bitem];
     796            0 :                 }
     797            0 :                 if (isNumeric) {
     798            0 :                     return bitem - aitem;
     799            0 :                 } else {
     800           26 :                     return aitem.localeCompare(bitem);
     801           26 :                 }
     802           26 :             });
     803            0 :             return direction === SortByDirection.asc ? sortedRows : sortedRows.reverse();
     804           41 :         };
     805              : 
     806           43 :         const card = (
     807           43 :             <Card id="containers-containers" className="containers-containers">
     808           43 :                 <CardHeader actions={{ actions: filterRunning }}>
     809           43 :                     <CardTitle><Content component={ContentVariants.h1}>{_("Containers")}</Content></CardTitle>
     810           43 :                 </CardHeader>
     811           43 :                 <CardBody>
     812           43 :                     <Flex direction={{ default: 'column' }}>
     813           43 :                         {(this.props.containers === null || this.props.pods === null)
     814           43 :                             ? <ListingTable variant='compact'
     815           43 :                                             aria-label={_("Containers")}
     816           43 :                                             emptyCaption={emptyCaption}
     817           43 :                                             columns={columnTitles}
     818           43 :                                             sortMethod={sortRows}
     819           43 :                                             rows={[]}
     820           43 :                                             sortBy={{ index: 0, direction: SortByDirection.asc }} />
     821           43 :                             : Object.keys(partitionedContainers)
     822            7 :                                     .sort((a, b) => {
     823            0 :                                         if (a == "no-pod") return -1;
     824            5 :                                         else if (b == "no-pod") return 1;
     825              : 
     826              :                                         // User pods are in front of system ones
     827            2 :                                         if (this.props.pods[a].uid !== this.props.pods[b].uid)
     828            0 :                                             return this.props.pods[a].uid === 0 ? 1 : -1;
     829            0 :                                         return this.props.pods[a].Name > this.props.pods[b].Name ? 1 : -1;
     830            7 :                                     })
     831           43 :                                     .map(section => {
     832           43 :                                         const tableProps = {};
     833           41 :                                         const rows = partitionedContainers[section].map(container => {
     834           41 :                                             return this.renderRow(this.props.containersStats, container,
     835           41 :                                                                   localImages);
     836           41 :                                         });
     837           43 :                                         let caption;
     838           43 :                                         let podStatus;
     839           43 :                                         let pod;
     840           43 :                                         let isPodService = false;
     841           43 :                                         let con;
     842           10 :                                         if (section !== 'no-pod') {
     843           10 :                                             pod = this.props.pods[section];
     844           10 :                                             con = this.props.users.find(u => u.uid === pod.uid).con;
     845           10 :                                             tableProps['aria-label'] = cockpit.format("Containers of pod $0", pod.Name);
     846           10 :                                             podStatus = pod.Status;
     847           10 :                                             isPodService = Boolean(pod.Labels?.PODMAN_SYSTEMD_UNIT);
     848           10 :                                             caption = pod.Name;
     849            8 :                                         } else {
     850           41 :                                             tableProps['aria-label'] = _("Containers");
     851           41 :                                         }
     852              : 
     853           43 :                                         const actions = caption && (
     854           10 :                                             <>
     855           10 :                                                 <Badge isRead className={"ct-badge-pod-" + podStatus.toLowerCase()}>{_(podStatus)}</Badge>
     856           10 :                                                 {!isPodService &&
     857            8 :                                                 <Button variant="secondary"
     858            8 :                                                         className="create-container-in-pod"
     859            8 :                                                         isDisabled={nonIntermediateImages === null}
     860            2 :                                                         onClick={() => createContainer(this.props.pods[section])}>
     861            8 :                                                     {_("Create container in pod")}
     862            8 :                                                 </Button>}
     863           10 :                                                 <PodActions con={con}
     864           10 :                                                             onAddNotification={this.props.onAddNotification}
     865           10 :                                                             pod={pod}
     866           10 :                                                             isPodService={isPodService}
     867           10 :                                                 />
     868           10 :                                             </>
     869              :                                         );
     870           43 :                                         return (
     871           43 :                                             <Card key={'table-' + section}
     872            8 :                                              id={'table-' + (section == "no-pod" ? section : this.props.pods[section].Name)}
     873           43 :                                              isPlain={section == "no-pod"}
     874           43 :                                              className="container-pod"
     875           43 :                                              isClickable
     876           43 :                                              isSelectable>
     877           10 :                                                 {caption && <CardHeader actions={{ actions, className: "panel-actions" }}>
     878           10 :                                                     <CardTitle>
     879           10 :                                                         <Flex justifyContent={{ default: 'justifyContentFlexStart' }}>
     880           10 :                                                             <h3 className='pod-name'>{caption}</h3>
     881           10 :                                                             <span>{_("pod")}</span>
     882            2 :                                                             {isPodService && <Badge className='ct-badge-service'>{_("service")}</Badge>}
     883           10 :                                                             {this.renderPodDetails(this.props.pods[section], podStatus)}
     884           10 :                                                         </Flex>
     885           10 :                                                     </CardTitle>
     886           10 :                                                 </CardHeader>}
     887           43 :                                                 <ListingTable variant='compact'
     888            8 :                                                           emptyCaption={section == "no-pod" ? emptyCaption : emptyCaptionPod}
     889           43 :                                                           columns={columnTitles}
     890           43 :                                                           sortMethod={sortRows}
     891           43 :                                                           rows={rows}
     892           43 :                                                           {...tableProps} />
     893           43 :                                             </Card>
     894              :                                         );
     895           43 :                                     })}
     896           43 :                     </Flex>
     897           43 :                     {this.state.showPruneUnusedContainersModal &&
     898            2 :                     <PruneUnusedContainersModal
     899            2 :                       close={() => this.setState({ showPruneUnusedContainersModal: false })}
     900            2 :                       unusedContainers={unusedContainers}
     901            2 :                       onAddNotification={this.props.onAddNotification}
     902            2 :                       users={this.props.users} /> }
     903           43 :                 </CardBody>
     904           43 :             </Card>
     905              :         );
     906              : 
     907           43 :         return <div ref={this.cardRef}>{card}</div>;
     908           43 :     }
     909           43 : }
     910              : 
     911           43 : export default Containers;
        

Generated by: LCOV version 2.0-1