LCOV - code coverage report
Current view: top level - pkg/storaged - jobs-panel.jsx Coverage Total Hit
Test: cockpit Lines: 90.7 % 172 156
Test Date: 2026-07-03 14:25:32

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2017 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          113 : import cockpit from "cockpit";
       7          113 : import React from "react";
       8              : 
       9              : import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
      10              : import { DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
      11              : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
      12              : 
      13              : import { StorageButton } from "./storage-controls.jsx";
      14              : import { block_name, mdraid_name, lvol_name, format_delay } from "./utils.js";
      15              : 
      16          113 : const _ = cockpit.gettext;
      17              : 
      18              : /* Human readable descriptions of the symbolic "Operation"
      19              :  * property of job objects.  These are from the storaged
      20              :  * documentation at
      21              :  *
      22              :  * https://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Job.html
      23              :  */
      24              : 
      25          113 : const descriptions = {
      26          113 :     'ata-smart-selftest': _("SMART self-test of $target"),
      27          113 :     'drive-eject': _("Ejecting $target"),
      28          113 :     'encrypted-unlock': _("Unlocking $target"),
      29          113 :     'encrypted-lock': _("Locking $target"),
      30          113 :     'encrypted-modify': _("Modifying $target"),
      31          113 :     'encrypted-resize': _("Resizing $target"),
      32          113 :     'swapspace-start': _("Starting swapspace $target"),
      33          113 :     'swapspace-stop': _("Stopping swapspace $target"),
      34          113 :     'filesystem-mount': _("Mounting $target"),
      35          113 :     'filesystem-unmount': _("Unmounting $target"),
      36          113 :     'filesystem-modify': _("Modifying $target"),
      37          113 :     'filesystem-resize': _("Resizing $target"),
      38          113 :     'filesystem-check': _("Checking $target"),
      39          113 :     'filesystem-repair': _("Repairing $target"),
      40          113 :     'format-erase': _("Erasing $target"),
      41          113 :     'format-mkfs': _("Creating filesystem on $target"),
      42          113 :     'loop-setup': _("Setting up loop device $target"),
      43          113 :     'partition-modify': _("Modifying $target"),
      44          113 :     'partition-delete': _("Deleting $target"),
      45          113 :     'partition-create': _("Creating partition $target"),
      46          113 :     cleanup: _("Cleaning up for $target"),
      47          113 :     'ata-secure-erase': _("Securely erasing $target"),
      48          113 :     'ata-enhanced-secure-erase': _("Very securely erasing $target"),
      49          113 :     'md-raid-stop': _("Stopping MDRAID device $target"),
      50          113 :     'md-raid-start': _("Starting MDRAID device $target"),
      51          113 :     'md-raid-fault-device': _("Marking $target as faulty"),
      52          113 :     'md-raid-remove-device': _("Removing $target from MDRAID device"),
      53          113 :     'md-raid-create': _("Creating MDRAID device $target"),
      54          113 :     'mdraid-check-job': _("Checking MDRAID device $target"),
      55          113 :     'mdraid-repair-job': _("Checking and repairing MDRAID device $target"),
      56          113 :     'mdraid-recover-job': _("Recovering MDRAID device $target"),
      57          113 :     'mdraid-sync-job': _("Synchronizing MDRAID device $target"),
      58          113 :     'lvm-lvol-delete': _("Deleting $target"),
      59          113 :     'lvm-lvol-activate': _("Activating $target"),
      60          113 :     'lvm-lvol-deactivate': _("Deactivating $target"),
      61          113 :     'lvm-lvol-snapshot': _("Creating snapshot of $target"),
      62          113 :     'lvm-vg-create': _("Creating LVM2 volume group $target"),
      63          113 :     'lvm-vg-delete': _("Deleting LVM2 volume group $target"),
      64          113 :     'lvm-vg-add-device': _("Adding physical volume to $target"),
      65          113 :     'lvm-vg-rem-device': _("Removing physical volume from $target"),
      66          113 :     'lvm-vg-empty-device': _("Emptying $target"),
      67          113 :     'lvm-vg-create-volume': _("Creating logical volume $target"),
      68          113 :     'lvm-vg-rename': _("Renaming $target"),
      69          113 :     'lvm-vg-resize': _("Resizing $target")
      70          113 : };
      71              : 
      72           46 : function make_description(client, job) {
      73           46 :     let fmt = descriptions[job.Operation];
      74           46 :     if (!fmt)
      75            0 :         fmt = _("Operation '$operation' on $target");
      76              : 
      77           46 :     const target = job.Objects.map(function (path) {
      78           46 :         if (client.blocks[path])
      79            1 :             return block_name(client.blocks[client.blocks[path].CryptoBackingDevice] || client.blocks[path]);
      80            2 :         else if (client.mdraids[path])
      81            0 :             return mdraid_name(client.mdraids[path]);
      82            0 :         else if (client.vgroups[path])
      83            0 :             return client.vgroups[path].Name;
      84            0 :         else if (client.lvols[path])
      85            0 :             return lvol_name(client.lvols[path]);
      86              :         else
      87            0 :             return _("unknown target");
      88           46 :     }).join(", ");
      89              : 
      90           46 :     return cockpit.format(fmt, { operation: job.Operation, target });
      91           46 : }
      92              : 
      93          113 : class JobRow extends React.Component {
      94            2 :     render() {
      95            2 :         const job = this.props.job;
      96              : 
      97            0 :         function cancel() {
      98            0 :             return job.Cancel({});
      99            0 :         }
     100              : 
     101            2 :         let remaining = null;
     102            2 :         if (job.ExpectedEndTime > 0) {
     103            2 :             const d = job.ExpectedEndTime / 1000 - this.props.now;
     104            2 :             if (d > 0)
     105            2 :                 remaining = format_delay(d);
     106            2 :         }
     107              : 
     108            2 :         return (
     109            2 :             <DataListItem>
     110            2 :                 <DataListItemRow>
     111            2 :                     <DataListItemCells
     112            2 :                         dataListCells={[
     113            2 :                             <DataListCell key="spinner" isFilled={false}>
     114            2 :                                 <Spinner size="lg" />
     115            2 :                             </DataListCell>,
     116            2 :                             <DataListCell key="desc" className="job-description" isFilled={false}>
     117            2 :                                 {make_description(this.props.client, job)}
     118            2 :                             </DataListCell>,
     119            2 :                             <DataListCell key="progress" isFilled={false}>
     120            2 :                                 {job.ProgressValid && (job.Progress * 100).toFixed() + "%"}
     121            2 :                             </DataListCell>,
     122            2 :                             <DataListCell key="remaining" isFilled={false}>
     123            2 :                                 {remaining}
     124            2 :                             </DataListCell>,
     125            2 :                             <DataListCell key="job-action" isFilled={false} alignRight>
     126            0 :                                 { job.Cancelable ? <StorageButton onClick={cancel}>{_("Cancel")}</StorageButton> : null }
     127            2 :                             </DataListCell>,
     128            2 :                         ]}
     129            2 :                     />
     130            2 :                 </DataListItemRow>
     131            2 :             </DataListItem>
     132              :         );
     133            2 :     }
     134          113 : }
     135              : 
     136          113 : export class JobsPanel extends React.Component {
     137          112 :     constructor() {
     138          112 :         super();
     139          112 :         this.reminder = null;
     140          112 :     }
     141              : 
     142           87 :     componentWillUnmount() {
     143            4 :         if (this.reminder) {
     144            4 :             window.clearTimeout(this.reminder);
     145            4 :             this.reminder = null;
     146            4 :         }
     147           87 :     }
     148              : 
     149          112 :     render() {
     150          112 :         const client = this.props.client;
     151          112 :         const path_jobs = client.path_jobs[this.props.path] || [];
     152          112 :         const server_now = new Date().getTime() + client.time_offset;
     153              : 
     154            0 :         function cmp_job(job_a, job_b) {
     155            0 :             return job_a.StartTime - job_b.StartTime;
     156            0 :         }
     157              : 
     158            3 :         function job_is_stable(job) {
     159            3 :             const age_ms = server_now - job.StartTime / 1000;
     160            3 :             if (age_ms >= 2000)
     161            0 :                 return true;
     162              : 
     163            2 :             if (job.ExpectedEndTime > 0 && (job.ExpectedEndTime / 1000 - server_now) >= 2000)
     164            2 :                 return true;
     165              : 
     166            3 :             return false;
     167            3 :         }
     168              : 
     169          112 :         let jobs = [];
     170          112 :         let have_reminder = false;
     171           15 :         for (const j of path_jobs) {
     172           14 :             if (job_is_stable(j)) {
     173           14 :                 jobs.push(j);
     174           14 :             } else if (!have_reminder) {
     175              :                 // If there is a unstable job, we have to check again in a bit since being
     176              :                 // stable or not depends on the current time.
     177           15 :                 if (this.reminder)
     178           15 :                     window.clearTimeout(this.reminder);
     179            3 :                 this.reminder = window.setTimeout(() => { this.setState({}) }, 1000);
     180           15 :                 have_reminder = true;
     181           15 :             }
     182           15 :         }
     183              : 
     184          112 :         if (jobs.length === 0)
     185          112 :             return null;
     186              : 
     187           14 :         jobs = jobs.sort(cmp_job);
     188              : 
     189           14 :         return (
     190           14 :             <CardBody className="contains-list">
     191           14 :                 <DataList isCompact aria-label={_("Jobs")}>
     192            2 :                     { jobs.map(j => <JobRow key={j.path} client={client} job={j} now={server_now} />) }
     193           14 :                 </DataList>
     194           14 :             </CardBody>
     195              :         );
     196          112 :     }
     197          113 : }
     198              : 
     199           49 : export function job_progress_wrapper(client, path1, path2) {
     200           49 :     return function (vals, progress_callback, action_function) {
     201           49 :         function client_changed() {
     202           49 :             const jobs = client.path_jobs[path1] || client.path_jobs[path2];
     203           45 :             if (jobs && jobs.length > 0) {
     204           45 :                 const job = jobs[0];
     205           45 :                 let desc = make_description(client, job);
     206           45 :                 if (job.ProgressValid)
     207            2 :                     desc += cockpit.format(" ($0%)", (job.Progress * 100).toFixed());
     208            0 :                 progress_callback(desc, job.Cancelable ? () => job.Cancel({}) : null);
     209           45 :             } else {
     210           49 :                 progress_callback(null, null);
     211           49 :             }
     212           49 :         }
     213              : 
     214           49 :         client.addEventListener("changed", client_changed);
     215           49 :         return action_function(vals, progress_callback)
     216           49 :                 .finally(() => { client.removeEventListener("changed", client_changed) });
     217           49 :     };
     218           49 : }
        

Generated by: LCOV version 2.0-1