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 16 : for (const j of path_jobs) {
172 15 : if (job_is_stable(j)) {
173 15 : jobs.push(j);
174 15 : } 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 16 : if (this.reminder)
178 16 : window.clearTimeout(this.reminder);
179 3 : this.reminder = window.setTimeout(() => { this.setState({}) }, 1000);
180 16 : have_reminder = true;
181 16 : }
182 16 : }
183 :
184 112 : if (jobs.length === 0)
185 112 : return null;
186 :
187 15 : jobs = jobs.sort(cmp_job);
188 :
189 15 : return (
190 15 : <CardBody className="contains-list">
191 15 : <DataList isCompact aria-label={_("Jobs")}>
192 2 : { jobs.map(j => <JobRow key={j.path} client={client} job={j} now={server_now} />) }
193 15 : </DataList>
194 15 : </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 : }
|