Line data Source code
1 : /*
2 : * Copyright (C) 2018 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 : /* STORAGE DIALOGS
7 :
8 : To show a modal dialog, make a call like this:
9 :
10 : dialog_show({ Title: _("What is your name?"),
11 : Fields: [
12 : TextInput("name", _("Name"),
13 : { validate: val => (val == ""? _("Name can't be empty") : null) })
14 : ]
15 : Action: {
16 : Title: _("Ok"),
17 : action: vals => { console.log("Hello, " + vals.name + "!"); }
18 : }
19 : });
20 :
21 : The call to dialog_show will open the dialog and return
22 : immediately. Later, when the user clicks on "Ok", the "action"
23 : function will be called with the values of the dialog fields. The
24 : action function usually returns a promise, although it does not in
25 : the example above. When that promise resolves, the dialog is
26 : closed. When the promise is rejected, it's error is displayed in
27 : the dialog, and the dialog stays open.
28 :
29 : Fields are described by calling functions such as TextInput. A
30 : number of generic ones are defined here, and you can define more
31 : specialized ones yourself.
32 :
33 : They are all called like this:
34 :
35 : FieldFunction(tag, title, { option: value, ... })
36 :
37 : The "tag" is used to uniquely identify this field in the dialog.
38 : The action function will receive the values of all fields in an
39 : object, and the tag of a field is the key in that object, for
40 : example. The tag is also used to interact with a field from tests.
41 :
42 : ACTION FUNCTIONS
43 :
44 : The action function is called like this:
45 :
46 : action(values, progress_callback)
47 :
48 : The "values" parameter contains the validated values of the dialog
49 : fields and the "progress_callback" can be called by the action function
50 : to update the progress information in the dialog while it runs.
51 :
52 : The progress callback should be called like this:
53 :
54 : progress_callback(message, cancel_callback)
55 :
56 : The "message" will be displayed in the dialog and if "cancel_callback" is
57 : not null, the Cancel button in the dialog will be enabled and
58 : "cancel_callback" will be called when the user clicks it.
59 :
60 : The return value of the action function is normally a promise. When
61 : it is resolved, the dialog is closed. When it is rejected the value
62 : given in the rejection is displayed as an error in the dialog.
63 :
64 : If the error value is a string, it is displayed as a global failure
65 : message. When it is an object, it contains errors for individual
66 : fields in this form:
67 :
68 : { tag1: message, tag2: message }
69 :
70 : As a special case, when "message" is "true", the field is rendered
71 : as having an error (with a red outline, say), but without any
72 : directly associated text. The idea is that a group of fields is in
73 : error, and the error message for all of them is shown below the last
74 : one in the group.
75 :
76 : COMMON FIELD OPTIONS
77 :
78 : Each field function describes its options. However, there are some
79 : options that apply to all fields:
80 :
81 : - value
82 :
83 : The initial value of the field.
84 :
85 : - visible: vals -> boolean
86 :
87 : This function determines whether the field is shown or not.
88 :
89 : - validate: (val, vals) -> null-or-error-string (or promise)
90 :
91 : The validate function receives the current value of the field and
92 : should return "null" (or something falsey) when that value is
93 : acceptable. Otherwise, it should return a suitable error message.
94 :
95 : The second argument has all values of all fields, in case you need
96 : to look at more than one field.
97 :
98 : It is permissible to overwrite fields of "vals" to change the final
99 : value of a field.
100 :
101 : The validate function can also return a promise which resolves to
102 : null or an error message. If that promise is rejected, that error
103 : is shown globally in the dialog as if the action function had
104 : failed.
105 :
106 : The validate function will only be called for currently visible
107 : fields.
108 :
109 : - widest_title
110 :
111 : This is a hack to force the column of titles to be a certain
112 : minimum width, namely the width of the widest_title. This matters
113 : when there are rows that are only sometimes visible and the layout
114 : would jump around when they change visibility.
115 :
116 : Technically, the first column of a row shows the "title" but is as
117 : wide as its "widest_title". The idea is that you put the widest
118 : title of all fields in the widest_title option of one of the rows
119 : that are always visible.
120 :
121 : - explanation
122 :
123 : A test to show below the field, as an explanation.
124 :
125 : RUNNING TASKS AND DYNAMIC UPDATES
126 :
127 : The dialog_show function returns an object that can be used to interact
128 : with the dialog in various ways while it is open.
129 :
130 : dlg = dialog_show(...)
131 :
132 : One can run asynchronous tasks:
133 :
134 : dlg.run("title", promise)
135 :
136 : This will disable the footer buttons and wait for promise to be resolved
137 : or rejected while showing "title" and a spinner.
138 :
139 : One can set field values and options:
140 :
141 : dlg.set_values({ tag1: value1, tag2: value2, ... })
142 : dlg.set_options(tag, { opt1: value1, opt2: value2, ... })
143 :
144 : It is also possible to specify a "update" function when creating the dialog:
145 :
146 : dialog_show({ ...
147 : update: function (dlg, vals, trigger) { }
148 : ... })
149 :
150 : This function is called whenever the values of fields are changed. The
151 : "trigger" argument is the tag of the field that has just been changed.
152 :
153 : DEFINING NEW FIELD TYPES
154 :
155 : To define a new field type, just define a new function that follows
156 : a few rules. Here is TextInput:
157 :
158 : export const TextInput = (tag, title, options) => {
159 : return {
160 : tag: tag,
161 : title: title,
162 : options: options,
163 : initial_value: "",
164 :
165 : render: (val, change) =>
166 : <input data-field={tag}
167 : className="form-control" type="text" value={val}
168 : onChange={event => change(event.target.value)}/>
169 : }
170 : }
171 :
172 : As you can see, a field function should return an object with a
173 : couple of fields. The "tag", "title", and "options" field just
174 : store the parameters to the field function. The rest are these:
175 :
176 : - initial_value
177 :
178 : This is the initial value of the field.
179 :
180 : - render: (val, change) -> React components
181 :
182 : This should render the value part of the field, that is, the second
183 : column in the table layout. The title is in the first column and
184 : is rendered by the generic dialog machinery.
185 :
186 : The "val" parameter is the current value and you should make sure
187 : that the DOM element really shows that value, and not something
188 : that might have left behind by previous user interactions.
189 :
190 : The "change" parameter is a function that should be called with a
191 : new value for the field whenever the user has interacted with it.
192 :
193 : For the benefits of the integration tests, the DOM elements should
194 : also contain "data-field" and maybe a "data-field-type" attributes. The
195 : "data-field" value should be that tag of the field, and
196 : "data-field-type" type is used by the tests to know how to interact
197 : with the field. If you find to need it, just pick a reasonable value
198 : and extend the test suite to handle it.
199 :
200 : This function is not called at all for invisible fields.
201 : */
202 :
203 113 : import cockpit from "cockpit";
204 :
205 113 : import React, { useState } from "react";
206 : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
207 : import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
208 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
209 : import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
210 : import { DataList, DataListCell, DataListCheck, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
211 : import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
212 : import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
213 : import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
214 : import { Slider } from "@patternfly/react-core/dist/esm/components/Slider/index.js";
215 : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
216 : import { Split } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
217 : import { TextInput as TextInputPF4 } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
218 : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
219 : import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
220 : import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
221 : import { ExclamationTriangleIcon, InfoIcon, HelpIcon, EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
222 : import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
223 : import { Table, Tbody, Tr, Td } from '@patternfly/react-table';
224 :
225 : import { TypeaheadSelect } from "cockpit-components-typeahead-select";
226 : import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
227 : import { ListingTable } from "cockpit-components-table.jsx";
228 : import { FormHelper } from "cockpit-components-form-helper";
229 :
230 : import {
231 : decode_filename, fmt_size, block_name, format_size_and_text, format_delay, for_each_async, get_byte_units,
232 : BTRFS_TOOL_MOUNT_PATH
233 : } from "./utils.js";
234 : import { fmt_to_fragments } from "utils.jsx";
235 : import client from "./client.js";
236 :
237 : import fsys_is_empty_sh from "./fsys-is-empty.sh";
238 :
239 113 : const _ = cockpit.gettext;
240 :
241 88 : function make_rows(fields, values, errors, onChange) {
242 88 : return fields.map((f, i) => <Row key={i} field={f} values={values} errors={errors} onChange={onChange} />)
243 88 : .filter(r => r);
244 88 : }
245 :
246 88 : function is_visible(field, values) {
247 74 : return !field.options || field.options.visible == undefined || field.options.visible(values);
248 88 : }
249 :
250 88 : const Field = ({ field, values, errors, onChange }) => {
251 88 : const { tag, options } = field;
252 :
253 88 : if (!is_visible(field, values))
254 10 : return null;
255 :
256 16 : const error = errors && errors[tag];
257 88 : const explanation = options && options.explanation;
258 16 : const validated = (tag && errors && errors[tag]) ? 'error' : 'default';
259 :
260 80 : function change(val) {
261 80 : values[tag] = val;
262 80 : onChange(tag);
263 80 : }
264 :
265 88 : return (
266 88 : <>
267 88 : {field.render(values[tag], change, validated, error)}
268 88 : <FormHelper helperText={explanation} helperTextInvalid={validated && error} />
269 88 : </>);
270 88 : };
271 :
272 88 : const Row = ({ field, values, errors, onChange }) => {
273 88 : const { title, options } = field;
274 :
275 88 : if (!is_visible(field, values))
276 70 : return null;
277 :
278 88 : const field_elts = <Field field={field} values={values} errors={errors} onChange={onChange} />;
279 88 : let nested_elts = [];
280 58 : if (options && options.nested_fields) {
281 58 : if (field.is_group)
282 9 : nested_elts = options.nested_fields.map(f => <Field key={f.tag}
283 9 : field={f}
284 9 : values={values}
285 9 : errors={errors}
286 6 : onChange={onChange} />);
287 : else
288 55 : nested_elts = make_rows(options.nested_fields, values, errors, onChange);
289 58 : }
290 :
291 27 : if (title || title == "") {
292 88 : let titleLabel = title;
293 :
294 88 : if (options.widest_title)
295 88 : titleLabel = (
296 1 : <>
297 1 : <div className="widest-title">{options.widest_title}</div>
298 1 : <div>{title}</div>
299 1 : </>
300 : );
301 88 : return (
302 88 : <FormGroup label={titleLabel} hasNoPaddingTop={field.hasNoPaddingTop}>
303 88 : { field_elts }
304 88 : { nested_elts }
305 88 : </FormGroup>
306 : );
307 7 : } else if (!field.bare) {
308 7 : return (
309 7 : <FormGroup hasNoPaddingTop={field.hasNoPaddingTop}>
310 7 : { field_elts }
311 7 : { nested_elts }
312 7 : </FormGroup>
313 : );
314 7 : } else
315 2 : return field_elts;
316 88 : };
317 :
318 91 : const Body = ({ body, teardown, fields, values, errors, isFormHorizontal, onChange }) => {
319 91 : let error_alert = null;
320 :
321 9 : if (errors && errors.toString() != "[object Object]") {
322 : // This is a global error from a failed action
323 9 : error_alert = <Alert variant='danger' isInline title={errors.toString()} />;
324 9 : errors = null;
325 9 : }
326 :
327 91 : return (
328 91 : <>
329 91 : { error_alert }
330 91 : { body || null }
331 91 : { fields.length > 0
332 90 : ? <Form onSubmit={apply_modal_dialog}
333 90 : isHorizontal={isFormHorizontal !== false}>
334 90 : { make_rows(fields, values, errors, onChange) }
335 90 : </Form>
336 41 : : null }
337 91 : { teardown }
338 91 : </>
339 : );
340 91 : };
341 :
342 2 : const ExtraConfirmation = ({ text, onChange }) => {
343 2 : const [confirmed, setConfirmed] = useState(false);
344 :
345 2 : return (
346 2 : <Checkbox isChecked={confirmed}
347 2 : id="dialog-confirm"
348 2 : label={text}
349 2 : onChange={(_, val) => {
350 2 : setConfirmed(val);
351 2 : onChange(val);
352 2 : }} />);
353 2 : };
354 :
355 91 : function flatten_fields(fields) {
356 91 : return fields.reduce(
357 88 : (acc, val) => acc.concat([val]).concat(val.options && val.options.nested_fields
358 58 : ? flatten_fields(val.options.nested_fields)
359 88 : : []),
360 91 : []);
361 91 : }
362 :
363 91 : export const dialog_open = (def) => {
364 41 : const nested_fields = def.Fields || [];
365 91 : const fields = flatten_fields(nested_fields);
366 91 : const values = { };
367 91 : let confirmation = null;
368 91 : let confirmed = false;
369 91 : let errors = null;
370 :
371 88 : fields.forEach(f => { values[f.tag] = f.initial_value });
372 :
373 : // We reconstruct the body every time the values change so that it
374 : // will be re-rendered. This could be done with some state in the
375 : // Body component maybe, but we also want the values up here so
376 : // that we can pass them to validate and the action function.
377 :
378 86 : const update = () => {
379 86 : dlg.setProps(props());
380 86 : };
381 :
382 91 : const props = () => {
383 91 : return {
384 91 : id: "dialog",
385 91 : title: def.Title,
386 49 : titleIconVariant: (def.Action && (def.Action.Danger || def.Action.DangerButton)) ? "warning" : null,
387 91 : body: <Body body={def.Body}
388 91 : teardown={def.Teardown}
389 91 : fields={nested_fields}
390 91 : values={values}
391 91 : errors={errors}
392 91 : isFormHorizontal={def.isFormHorizontal}
393 80 : onChange={trigger => {
394 80 : errors = null;
395 80 : if (def.update)
396 73 : def.update(self, values, trigger);
397 80 : update();
398 80 : }} />
399 91 : };
400 91 : };
401 :
402 73 : const update_footer = (running_title, running_promise) => {
403 73 : dlg.setFooterProps(footer_props(running_title, running_promise));
404 73 : };
405 :
406 85 : function run_action(progress_callback, variant) {
407 85 : const func = () => {
408 85 : return validate(variant)
409 85 : .then(validated_values => {
410 85 : const visible_values = { variant };
411 82 : fields.forEach(f => {
412 82 : if (is_visible(f, values))
413 82 : visible_values[f.tag] = validated_values[f.tag];
414 82 : });
415 85 : if (def.Action.wrapper)
416 52 : return def.Action.wrapper(visible_values, progress_callback,
417 36 : def.Action.action);
418 : else
419 69 : return def.Action.action(visible_values, progress_callback);
420 85 : })
421 22 : .catch(errs => {
422 7 : if (errs && errs.toString() != "[object Object]") {
423 : // Log errors from failed actions, for debugging and
424 : // to allow the test suite to catch known issues.
425 7 : console.warn(errs.toString());
426 7 : }
427 22 : errors = errs;
428 22 : update();
429 22 : update_footer();
430 22 : return Promise.reject();
431 22 : });
432 85 : };
433 85 : return client.run(func);
434 85 : }
435 :
436 91 : const footer_props = (running_title, running_promise) => {
437 91 : const actions = [];
438 :
439 91 : function add_action(variant) {
440 91 : actions.push({
441 91 : caption: variant.Title,
442 52 : style: actions.length == 0 ? "primary" : "secondary",
443 76 : danger: def.Action.Danger || def.Action.DangerButton,
444 91 : disabled: (running_promise != null ||
445 91 : (def.Action.disable_on_error &&
446 5 : errors && errors.toString() != "[object Object]") ||
447 5 : (confirmation && !confirmed)),
448 85 : clicked: progress_callback => run_action(progress_callback, variant.tag),
449 91 : });
450 91 : }
451 :
452 91 : if (def.Action) {
453 79 : if (def.Action.Title) {
454 79 : add_action({
455 79 : Title: def.Action.Title,
456 79 : tag: null,
457 79 : });
458 79 : }
459 :
460 59 : if (def.Action.Variants) {
461 59 : for (const v of def.Action.Variants) {
462 59 : add_action(v);
463 59 : }
464 59 : }
465 91 : }
466 :
467 91 : let extra = null;
468 5 : if (confirmation) {
469 5 : extra = <ExtraConfirmation text={confirmation}
470 2 : onChange={val => {
471 2 : confirmed = val;
472 2 : update_footer();
473 2 : }} />;
474 5 : } else if (def.Action && def.Action.Danger) {
475 64 : extra = (
476 64 : <div>
477 64 : <HelperText><HelperTextItem variant="error">{def.Action.Danger} </HelperTextItem></HelperText>
478 64 : </div>);
479 64 : }
480 :
481 91 : return {
482 91 : idle_message: (running_promise
483 72 : ? <>
484 72 : <span>{running_title}</span>
485 72 : <Spinner className="dialog-wait-ct-spinner" size="md" />
486 72 : </>
487 91 : : null),
488 91 : extra_element: extra,
489 91 : actions,
490 5 : cancel_button: def.Action ? {} : { text: _("Close"), variant: "secondary" }
491 91 : };
492 91 : };
493 :
494 85 : const validate = (variant) => {
495 : // The validation functions sometimes change the dialog values
496 : // for the benefit of the action functions. For example, a
497 : // SizeSlider will convert from "text plus unit" to a numeric
498 : // value during validation.
499 : //
500 : // However, if the action fails, we don't want the values in
501 : // the dialog to change. The SizeSlider would convert back
502 : // from a numeric value to "text plus unit", for example, and
503 : // that conversion might change what the user had type.
504 : //
505 : // So we make a copy of the dialog state and let the validate
506 : // and action functions work with that.
507 : //
508 85 : const validated_values = { ...values };
509 :
510 82 : return Promise.all(fields.map(f => {
511 82 : if (is_visible(f, values) && f.options && f.options.validate)
512 73 : return f.options.validate(validated_values[f.tag], validated_values, variant);
513 : else
514 79 : return null;
515 82 : })).then(results => {
516 85 : const errors = { };
517 85 : let scrolled = false;
518 82 : fields.forEach((f, i) => {
519 16 : if (results[i]) {
520 16 : if (!scrolled) {
521 16 : show_field(f.tag);
522 16 : scrolled = true;
523 16 : }
524 16 : errors[f.tag] = results[i];
525 16 : }
526 82 : });
527 85 : if (Object.keys(errors).length > 0)
528 18 : return Promise.reject(errors);
529 85 : return validated_values;
530 85 : });
531 85 : };
532 :
533 91 : const dlg = show_modal_dialog(props(), footer_props(null, null));
534 :
535 29 : function show_field(tag) {
536 29 : function scroll() {
537 29 : const field_element = document.querySelector('#dialog [data-field="' + tag + '"]');
538 29 : if (field_element)
539 29 : field_element.scrollIntoView({ behavior: "smooth", block: "nearest" });
540 29 : }
541 : // By the time show_field is called from the "update"
542 : // callback, newly visible fields don't exist yet in the
543 : // DOM, so delay the scrolling a bit.
544 29 : window.setTimeout(scroll, 10);
545 29 : }
546 :
547 91 : const self = {
548 71 : run: (title, promise) => {
549 71 : update_footer(title, promise);
550 71 : promise.then(
551 71 : () => {
552 71 : update_footer(null, null);
553 71 : },
554 0 : (errs) => {
555 0 : if (errs) {
556 0 : errors = errs;
557 0 : update();
558 0 : }
559 0 : update_footer(null, null);
560 0 : });
561 71 : },
562 :
563 33 : set_values: (new_vals) => {
564 33 : Object.assign(values, new_vals);
565 33 : update();
566 33 : },
567 :
568 53 : get_value: (tag) => {
569 53 : return values[tag];
570 53 : },
571 :
572 44 : update_actions: (new_actions) => {
573 44 : Object.assign(def.Action, new_actions);
574 44 : update_footer(null, null);
575 44 : },
576 :
577 0 : set_nested_values: (key, new_vals) => {
578 0 : const updated = values[key];
579 0 : Object.assign(updated, new_vals);
580 0 : values[key] = updated;
581 0 : update();
582 0 : },
583 :
584 8 : get_options: (tag) => {
585 8 : for (const f of fields) {
586 8 : if (f.tag == tag) {
587 8 : return f.options;
588 8 : }
589 0 : }
590 8 : },
591 :
592 22 : set_options: (tag, new_options) => {
593 22 : fields.forEach(f => {
594 22 : if (f.tag == tag) {
595 22 : Object.assign(f.options, new_options);
596 22 : update();
597 22 : }
598 22 : });
599 22 : },
600 :
601 71 : set_attribute: (name, value) => {
602 71 : def[name] = value;
603 71 : update();
604 71 : },
605 :
606 5 : add_danger: (danger) => {
607 5 : def.Action.Danger = <>{def.Action.Danger} {danger}</>;
608 5 : update();
609 5 : },
610 :
611 8 : need_confirmation: (conf) => {
612 8 : confirmation = conf;
613 8 : confirmed = false;
614 8 : def.Action.Danger = null;
615 8 : def.Action.DangerButton = true;
616 8 : update_footer();
617 8 : },
618 :
619 91 : show_field,
620 :
621 0 : close: () => {
622 0 : dlg.footerProps.dialog_done();
623 0 : }
624 91 : };
625 :
626 55 : for_each_async(def.Inits || [],
627 71 : init => {
628 71 : if (init) {
629 71 : const promise = init.func(self);
630 71 : self.run(init.title, promise);
631 71 : return promise;
632 71 : } else
633 51 : return Promise.resolve();
634 71 : });
635 :
636 91 : return self;
637 91 : };
638 :
639 : /* GENERIC FIELD TYPES
640 : */
641 :
642 82 : export const TextInput = (tag, title, options) => {
643 82 : return {
644 82 : tag,
645 82 : title,
646 82 : options,
647 69 : initial_value: options.value || "",
648 :
649 82 : render: (val, change, validated) =>
650 82 : <TextInputPF4 data-field={tag} data-field-type="text-input"
651 82 : validated={validated}
652 82 : aria-label={title}
653 82 : value={val}
654 82 : isDisabled={options.disabled}
655 62 : onChange={(_event, value) => change(value)} />
656 82 : };
657 82 : };
658 :
659 20 : const PassInputElement = ({ tag, title, options, val, change, validated }) => {
660 20 : const [show, setShow] = useState(false);
661 :
662 20 : return (
663 20 : <InputGroup>
664 20 : <TextInputPF4 data-field={tag} data-field-type="text-input"
665 20 : validated={validated}
666 20 : disabled={options.disabled}
667 20 : aria-label={title}
668 12 : autoComplete={options.new_password ? "new-password" : null}
669 0 : type={show ? "text" : "password"}
670 20 : value={val}
671 20 : onChange={(_event, value) => change(value)} />
672 20 : <Button variant="control"
673 0 : onClick={() => setShow(!show)}
674 20 : isDisabled={options.disabled}>
675 0 : { show ? <EyeSlashIcon /> : <EyeIcon /> }
676 20 : </Button>
677 20 : </InputGroup>);
678 20 : };
679 :
680 61 : export const PassInput = (tag, title, options) => {
681 61 : return {
682 61 : tag,
683 61 : title,
684 61 : options,
685 61 : initial_value: options.value || "",
686 :
687 20 : render: (val, change, validated) =>
688 20 : <PassInputElement tag={tag}
689 20 : title={title}
690 20 : options={options}
691 20 : val={val}
692 20 : change={change}
693 20 : validated={validated} />
694 61 : };
695 61 : };
696 :
697 5 : const TypeAheadSelectElement = ({ value, options, change }) => {
698 5 : return (
699 5 : <TypeaheadSelect toggleProps={ { id: "nfs-path-on-server" } }
700 5 : isScrollable
701 5 : isCreatable
702 2 : createOptionMessage={val => cockpit.format(_("Use $0"), val)}
703 5 : selected={value}
704 3 : onSelect={(_, value) => change(value)}
705 0 : onClearSelection={() => change("")}
706 5 : isDisabled={options.disabled}
707 3 : selectOptions={options.choices.map(entry => ({ value: entry, content: entry }))} />
708 : );
709 5 : };
710 :
711 5 : export const ComboBox = (tag, title, options) => {
712 5 : return {
713 5 : tag,
714 5 : title,
715 5 : options,
716 5 : initial_value: options.value || "",
717 :
718 5 : render: (val, change, validated) => {
719 5 : return (
720 5 : <div data-field={tag} data-field-type="combobox">
721 5 : <TypeAheadSelectElement value={val} options={options} change={change} />
722 5 : </div>
723 : );
724 5 : }
725 5 : };
726 5 : };
727 :
728 74 : export const SelectOne = (tag, title, options) => {
729 74 : return {
730 74 : tag,
731 74 : title,
732 74 : options,
733 2 : initial_value: options.value || options.choices[0].value,
734 :
735 73 : render: (val, change, validated) => {
736 73 : return (
737 73 : <div data-field={tag} data-field-type="select" data-value={val}>
738 73 : <FormSelect value={val} aria-label={tag}
739 73 : validated={validated}
740 55 : onChange={(_, value) => change(value)}>
741 73 : { options.choices.map(c => <FormSelectOption value={c.value} isDisabled={c.disabled}
742 73 : key={c.title} label={c.title} />) }
743 73 : </FormSelect>
744 73 : </div>
745 : );
746 73 : }
747 74 : };
748 74 : };
749 :
750 11 : export const SelectOneRadio = (tag, title, options) => {
751 11 : return {
752 11 : tag,
753 11 : title,
754 11 : options,
755 0 : initial_value: options.value || options.choices[0].value,
756 11 : hasNoPaddingTop: true,
757 :
758 7 : render: (val, change) => {
759 0 : const vertical = options?.vertical || false;
760 7 : const fields = options.choices.map(c => (
761 7 : <Radio key={c.value} isChecked={val == c.value} data-data={c.value}
762 7 : id={tag + '.' + c.value}
763 5 : onChange={() => change(c.value)} label={c.title} />));
764 :
765 7 : if (vertical) {
766 7 : return (
767 7 : <div data-field={tag} data-field-type="select-radio">
768 7 : {fields}
769 7 : </div>);
770 0 : } else {
771 0 : return (
772 0 : <Split hasGutter data-field={tag} data-field-type="select-radio">
773 0 : {fields}
774 0 : </Split>);
775 0 : }
776 7 : }
777 11 : };
778 11 : };
779 :
780 1 : export const SelectRow = (tag, headers, options) => {
781 1 : return {
782 1 : tag,
783 1 : title: null,
784 1 : options,
785 1 : initial_value: options.value || options.choices[0].value,
786 :
787 1 : render: (val, change) => {
788 1 : return (
789 1 : <table data-field={tag} data-field-type=" select-row" className="dialog-select-row-table">
790 1 : <thead>
791 1 : <tr>{headers.map(h => <th key={h}>{h}</th>)}</tr>
792 1 : </thead>
793 1 : <tbody>
794 1 : { options.choices.map(row => {
795 1 : return (
796 1 : <tr key={row.value}
797 0 : onMouseDown={ev => { if (ev && ev.button === 0) change(row.value); }}
798 0 : className={row.value == val ? "highlight-ct" : ""}>
799 1 : {row.columns.map(c => <td key={c}>{c}</td>)}
800 1 : </tr>
801 : );
802 1 : })
803 : }
804 1 : </tbody>
805 1 : </table>
806 : );
807 1 : }
808 1 : };
809 1 : };
810 :
811 25 : function nice_block_name(block) {
812 25 : return block_name(client.blocks[block.CryptoBackingDevice] || block);
813 25 : }
814 :
815 27 : export const SelectSpaces = (tag, title, options) => {
816 27 : return {
817 27 : tag,
818 27 : title,
819 27 : options,
820 26 : initial_value: options.value || [],
821 27 : hasNoPaddingTop: options.spaces.length == 0,
822 :
823 26 : render: (val, change) => {
824 26 : if (options.spaces.length === 0)
825 2 : return <span className="text-danger">{options.empty_warning}</span>;
826 :
827 25 : return (
828 25 : <DataList isCompact
829 25 : data-field={tag} data-field-type="select-spaces">
830 25 : { options.spaces.map(spc => {
831 25 : const selected = (val.indexOf(spc) >= 0);
832 0 : const block = spc.block ? nice_block_name(spc.block) : "";
833 6 : const desc = block === spc.desc ? "" : spc.desc;
834 :
835 23 : const on_change = (_event, checked) => {
836 : // Be careful to keep "val" in the same order as "options.spaces".
837 23 : if (checked && !selected)
838 23 : change(options.spaces.filter(v => val.indexOf(v) >= 0 || v == spc));
839 4 : else if (!checked && selected)
840 4 : change(val.filter(v => (v != spc)));
841 23 : };
842 :
843 25 : const datalistcells = (
844 25 : <DataListItemCells
845 25 : dataListCells={[
846 25 : <DataListCell key="select-space-name" className="select-space-name">
847 25 : {format_size_and_text(spc.size, desc)}
848 25 : </DataListCell>,
849 25 : <DataListCell alignRight isFilled={false} key="select-space-details" className="select-space-details">
850 25 : {block}
851 25 : </DataListCell>,
852 25 : ]}
853 25 : />);
854 :
855 0 : const key = block || desc;
856 :
857 25 : return (
858 25 : <DataListItem data-space-name={key} key={key}>
859 25 : <DataListItemRow>
860 0 : <DataListCheck id={(spc.block ? spc.block.Device : spc.desc) + "-row-checkbox"}
861 25 : isDisabled={options.min_selected &&
862 7 : selected && val.length <= options.min_selected}
863 25 : isChecked={selected} onChange={on_change} />
864 0 : <label htmlFor={(spc.block ? spc.block.Device : spc.desc) + "-row-checkbox"}
865 25 : className='data-list-row-checkbox-label'>
866 25 : {datalistcells}
867 25 : </label>
868 25 : </DataListItemRow>
869 25 : </DataListItem>
870 : );
871 25 : })
872 : }
873 25 : </DataList>
874 : );
875 26 : }
876 27 : };
877 27 : };
878 :
879 : export const SelectSpace = (tag, title, options) => {
880 : return {
881 : tag,
882 : title,
883 : options,
884 : initial_value: null,
885 :
886 : render: (val, change) => {
887 : if (options.spaces.length === 0)
888 : return <span className="text-danger">{options.empty_warning}</span>;
889 :
890 : return (
891 : <DataList isCompact
892 : data-field={tag} data-field-type="select-spaces">
893 : { options.spaces.map(spc => {
894 : const block = spc.block ? nice_block_name(spc.block) : "";
895 : const desc = block === spc.desc ? "" : spc.desc;
896 : const on_change = (event) => {
897 : if (event.target.checked)
898 : change(spc);
899 : };
900 :
901 : const key = block || desc;
902 :
903 : return (
904 : <DataListItem data-space-name={key} key={key}>
905 : <DataListItemRow>
906 : <div className="pf-v6-c-data-list__item-control">
907 : <div className="pf-v6-c-data-list__check">
908 : <input type='radio' value={desc} name='space' checked={val == spc} onChange={on_change} />
909 : </div>
910 : </div>
911 : <DataListItemCells
912 : dataListCells={[
913 : <DataListCell key="select-space-name" className="select-space-name">
914 : {format_size_and_text(spc.size, desc)}
915 : </DataListCell>,
916 : <DataListCell alignRight isFilled={false} key="select-space-details" className="select-space-details">
917 : {block}
918 : </DataListCell>,
919 : ]}
920 : />
921 : </DataListItemRow>
922 : </DataListItem>
923 : );
924 : })
925 : }
926 : </DataList>
927 : );
928 : }
929 : };
930 : };
931 :
932 72 : const CheckBoxComponent = ({ tag, val, title, tooltip, disabled, update_function }) => {
933 72 : return (
934 72 : <Checkbox data-field={tag} data-field-type="checkbox"
935 72 : id={tag}
936 72 : isChecked={val}
937 72 : isDisabled={disabled}
938 72 : label={
939 72 : <>
940 72 : {title}
941 0 : { tooltip && <Popover bodyContent={tooltip}>
942 0 : <Button icon={<HelpIcon />} className="dialog-item-tooltip" variant="link" />
943 0 : </Popover>
944 : }
945 72 : </>
946 : }
947 11 : onChange={(_, v) => update_function(v)} />
948 : );
949 72 : };
950 :
951 79 : export const CheckBoxes = (tag, title, options) => {
952 79 : return {
953 79 : tag,
954 79 : title,
955 79 : options,
956 58 : initial_value: options.value || { },
957 79 : hasNoPaddingTop: true,
958 :
959 72 : render: (val, change) => {
960 72 : const fieldset = options.fields.map(field => {
961 72 : const ftag = tag + "." + field.tag;
962 50 : const fval = (val[field.tag] !== undefined) ? val[field.tag] : false;
963 14 : function fchange(newval) {
964 14 : val[field.tag] = newval;
965 14 : change(val);
966 14 : }
967 :
968 58 : if (field.type === undefined || field.type == "checkbox")
969 72 : return <CheckBoxComponent key={`checkbox-${ftag}`}
970 72 : tag={ftag}
971 72 : val={fval}
972 72 : title={field.title}
973 72 : disabled={field.disabled}
974 72 : tooltip={field.tooltip}
975 72 : options={options}
976 58 : update_function={fchange} />;
977 58 : else if (field.type == "checkboxWithInput")
978 58 : return <TextInputCheckedComponent key={`checkbox-with-text-${ftag}`}
979 58 : tag={ftag}
980 58 : val={fval}
981 58 : title={field.title}
982 0 : update_function={fchange} />;
983 : else
984 0 : return null;
985 72 : });
986 :
987 72 : if (options.fields.length == 1)
988 61 : return fieldset;
989 :
990 : // eslint-disable-next-line react/jsx-no-useless-fragment
991 58 : return <>{ fieldset }</>;
992 72 : }
993 79 : };
994 79 : };
995 :
996 58 : const TextInputCheckedComponent = ({ tag, val, title, update_function }) => {
997 58 : return (
998 58 : <div data-field={tag} data-field-type="text-input-checked" key={tag}>
999 58 : <Checkbox isChecked={val !== false}
1000 58 : id={tag}
1001 58 : label={title}
1002 0 : onChange={(_event, checked) => update_function(checked ? "" : false)} />
1003 4 : {val !== false && <TextInputPF4 id={tag + "-input"} value={val} onChange={(_event, value) => update_function(value)} />}
1004 58 : </div>
1005 : );
1006 58 : };
1007 :
1008 3 : export const Skip = (className, options) => {
1009 3 : return {
1010 3 : tag: false,
1011 3 : title: null,
1012 3 : options,
1013 3 : initial_value: false,
1014 :
1015 3 : render: () => {
1016 3 : return <div className={className} />;
1017 3 : }
1018 3 : };
1019 3 : };
1020 :
1021 12 : export const Message = (text, options) => {
1022 12 : return {
1023 12 : options,
1024 :
1025 2 : render: () => <HelperText><HelperTextItem icon={<InfoIcon />}>{text}</HelperTextItem></HelperText>,
1026 12 : };
1027 12 : };
1028 :
1029 20 : function size_slider_round(value, round) {
1030 20 : if (round) {
1031 20 : if (typeof round == "function")
1032 0 : value = round(value);
1033 : else
1034 20 : value = Math.round(value / round) * round;
1035 0 : } else {
1036 : // Only produce integers by default
1037 0 : value = Math.round(value);
1038 0 : }
1039 20 : return value;
1040 20 : }
1041 :
1042 113 : class SizeSliderElement extends React.Component {
1043 32 : constructor(props) {
1044 32 : super();
1045 32 : this.units = get_byte_units(props.value || props.max);
1046 32 : this.state = { unit: this.units.find(u => u.selected).factor };
1047 32 : }
1048 :
1049 32 : render() {
1050 32 : const { val, max, round, onChange, tag } = this.props;
1051 25 : const min = this.props.min || 0;
1052 32 : const { unit } = this.state;
1053 :
1054 2 : const change_slider = (_event, f) => {
1055 2 : onChange(Math.max(min, size_slider_round(f, round)));
1056 2 : };
1057 :
1058 20 : const change_text = (value) => {
1059 : /* We keep the literal string as the value and only
1060 : * interpret it below in the validate function inside
1061 : * SizeSlider. This allows people to freely interact with
1062 : * the text input without getting the text changed all the
1063 : * time by rounding, etc.
1064 : */
1065 20 : onChange({ text: value, unit });
1066 20 : };
1067 :
1068 32 : let slider_val;
1069 32 : let text_val;
1070 20 : if (val.text && val.unit) {
1071 20 : slider_val = Number(val.text) * val.unit;
1072 20 : text_val = val.text;
1073 20 : } else {
1074 32 : slider_val = val;
1075 32 : text_val = cockpit.format_number(val / unit);
1076 32 : }
1077 :
1078 20 : const change_unit = (_, u) => {
1079 20 : if (val.unit)
1080 1 : onChange({ text: val.text, unit: Number(u) });
1081 : else
1082 20 : onChange(size_slider_round(val / unit * Number(u), round));
1083 20 : this.setState({ unit: Number(u) });
1084 20 : };
1085 :
1086 32 : return (
1087 32 : <Grid hasGutter className="size-slider">
1088 32 : <GridItem span={12} sm={8}>
1089 32 : <Slider showBoundaries={false} min={min} max={max} step={(max - min) / 500}
1090 32 : value={slider_val} onChange={change_slider} />
1091 32 : </GridItem>
1092 32 : <GridItem span={6} sm={2}>
1093 20 : <TextInputPF4 className="size-text" value={text_val} onChange={(_event, value) => change_text(value)} />
1094 32 : </GridItem>
1095 32 : <GridItem span={6} sm={2}>
1096 32 : <FormSelect className="size-unit" value={unit} aria-label={tag} onChange={change_unit}>
1097 32 : { this.units.map(u => <FormSelectOption value={u.factor} key={u.name} label={u.name} />) }
1098 32 : </FormSelect>
1099 32 : </GridItem>
1100 32 : </Grid>
1101 : );
1102 32 : }
1103 113 : }
1104 :
1105 63 : export const SizeSlider = (tag, title, options) => {
1106 31 : const validate = (val, vals) => {
1107 31 : let msg = null;
1108 :
1109 20 : if (val.text && val.unit) {
1110 : // Convert to number.
1111 20 : const unit = val.unit;
1112 :
1113 20 : val = Number(val.text) * unit;
1114 :
1115 : // As a special case, if the user types something that
1116 : // looks like the maximum (or minimum) when formatted,
1117 : // always use exactly the maximum (or minimum). Otherwise
1118 : // we have the confusing possibility that with the exact
1119 : // same string in the text input, the size is sometimes
1120 : // too large (or too small) and sometimes not.
1121 :
1122 20 : const sanitize = (limit) => {
1123 20 : const fmt = cockpit.format_number(limit / unit);
1124 20 : const parse = +fmt * unit;
1125 :
1126 20 : if (val == parse)
1127 0 : val = limit;
1128 20 : };
1129 :
1130 15 : sanitize(all_options.min || 0);
1131 20 : sanitize(all_options.max);
1132 :
1133 20 : val = size_slider_round(val, all_options.round);
1134 20 : vals[tag] = val;
1135 20 : }
1136 :
1137 31 : if (isNaN(val))
1138 0 : msg = _("Size must be a number");
1139 31 : else if (val === 0)
1140 0 : msg = _("Size cannot be zero");
1141 31 : else if (val < 0)
1142 0 : msg = _("Size cannot be negative");
1143 29 : else if (!options.allow_infinite && val > options.max)
1144 1 : msg = _("Size is too large");
1145 14 : else if (options.min !== undefined && val < options.min)
1146 0 : msg = cockpit.format(_("Size must be at least $0"), fmt_size(options.min));
1147 31 : else if (options.validate)
1148 0 : msg = options.validate(val, vals);
1149 :
1150 31 : return msg;
1151 31 : };
1152 :
1153 : /* This object might be mutated by dialog.set_options(), so we
1154 : have to use it below for the 'max' option in order to pick up
1155 : changes to it.
1156 : */
1157 63 : const all_options = Object.assign({ }, options, { validate });
1158 :
1159 63 : return {
1160 63 : tag,
1161 63 : title,
1162 63 : options: all_options,
1163 39 : initial_value: options.value || options.max || 0,
1164 :
1165 32 : render: (val, change) => {
1166 32 : return (
1167 32 : <div data-field={tag} data-field-type="size-slider">
1168 32 : <SizeSliderElement val={val}
1169 32 : max={all_options.max}
1170 32 : min={all_options.min}
1171 32 : round={all_options.round}
1172 32 : tag={tag}
1173 32 : onChange={change} />
1174 32 : </div>
1175 : );
1176 32 : }
1177 63 : };
1178 63 : };
1179 :
1180 9 : export const Group = (title, fields) => {
1181 9 : return {
1182 9 : tag: null,
1183 9 : title,
1184 9 : is_group: true,
1185 9 : hasNoPaddingTop: true,
1186 9 : options: { nested_fields: fields },
1187 :
1188 9 : render: (val, change) => null,
1189 9 : };
1190 9 : };
1191 :
1192 1 : export const BlockingMessage = (usage) => {
1193 1 : const usage_desc = {
1194 1 : pvol: _("physical volume of LVM2 volume group"),
1195 1 : "mdraid-member": _("member of MDRAID device"),
1196 1 : vdo: _("backing device for VDO device"),
1197 1 : "stratis-pool-member": _("member of Stratis pool"),
1198 1 : mounted: _("Filesystem outside the target"),
1199 1 : "btrfs-device": _("device of btrfs volume"),
1200 1 : };
1201 :
1202 1 : const rows = [];
1203 1 : usage.forEach(use => {
1204 1 : if (use.blocking && use.block) {
1205 1 : const name = teardown_block_name(use);
1206 1 : rows.push({
1207 0 : columns: [name, use.location || "-", usage_desc[use.usage] || "-"]
1208 1 : });
1209 1 : }
1210 1 : });
1211 :
1212 1 : return (
1213 1 : <div>
1214 1 : <HelperText><HelperTextItem variant="warning">{_("This device is currently in use.")}</HelperTextItem></HelperText>
1215 1 : <ListingTable variant='compact'
1216 1 : columns={[
1217 1 : { title: _("Device") },
1218 1 : { title: _("Location") },
1219 1 : { title: _("Use") }
1220 1 : ]}
1221 1 : rows={rows} />
1222 1 : </div>);
1223 1 : };
1224 :
1225 28 : const UsersPopover = ({ users }) => {
1226 28 : const max = 10;
1227 3 : const services = users.filter(u => u.unit);
1228 3 : const processes = users.filter(u => u.pid);
1229 :
1230 28 : return (
1231 28 : <Popover
1232 28 : appendTo={document.body}
1233 28 : bodyContent={
1234 28 : <>
1235 28 : { services.length > 0
1236 1 : ? <>
1237 1 : <p><b>{_("Services using the location")}</b></p>
1238 1 : <List>
1239 1 : { services.slice(0, max).map((u, i) => <ListItem key={i}>{u.unit.replace(/\.service$/, "")}</ListItem>) }
1240 0 : { services.length > max ? <ListItem key={max}>...</ListItem> : null }
1241 1 : </List>
1242 1 : </>
1243 28 : : null
1244 : }
1245 1 : { services.length > 0 && processes.length > 0
1246 1 : ? <br />
1247 28 : : null
1248 : }
1249 28 : { processes.length > 0
1250 3 : ? <>
1251 3 : <p><b>{_("Processes using the location")}</b></p>
1252 3 : <List>
1253 3 : { processes.slice(0, max).map((u, i) => <ListItem key={i}>{u.comm} (user: {u.user}, pid: {u.pid})</ListItem>) }
1254 0 : { processes.length > max ? <ListItem key={max}>...</ListItem> : null }
1255 3 : </List>
1256 3 : </>
1257 28 : : null
1258 : }
1259 28 : </>}>
1260 3 : <Button icon={<ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />} variant="link" style={{ visibility: users.length == 0 ? "hidden" : null }}>
1261 28 : { "\n" }
1262 28 : {_("Currently in use")}
1263 28 : </Button>
1264 28 : </Popover>);
1265 28 : };
1266 :
1267 65 : function is_expected_unmount(usage, expect_single_unmount) {
1268 25 : return (expect_single_unmount && usage.length == 1 &&
1269 20 : usage[0].usage == "mounted" && usage[0].location == expect_single_unmount);
1270 65 : }
1271 :
1272 30 : const teardown_block_name = use => {
1273 30 : const block_stratis = client.blocks_stratis_fsys[use.block.path];
1274 30 : const block_btrfs = client.blocks_fsys_btrfs[use.block.path];
1275 30 : let name;
1276 5 : if (block_stratis) {
1277 5 : name = block_stratis.Devnode;
1278 0 : } else if (block_btrfs && use.name) {
1279 1 : name = use.name;
1280 0 : } else {
1281 18 : name = block_name(client.blocks[use.block.CryptoBackingDevice] || use.block);
1282 24 : }
1283 :
1284 30 : return name.replace(/^\/dev\//, "");
1285 30 : };
1286 :
1287 71 : export const TeardownMessage = (usage, expect_single_unmount) => {
1288 71 : if (!usage.Teardown)
1289 65 : return null;
1290 :
1291 6 : if (client.in_anaconda_mode() && !expect_single_unmount)
1292 6 : return <AnacondaTeardownMessage usage={usage} />;
1293 :
1294 39 : if (is_expected_unmount(usage, expect_single_unmount))
1295 20 : return <StopProcessesMessage mount_point={expect_single_unmount} users={usage[0].users} />;
1296 :
1297 29 : const rows = [];
1298 28 : usage.forEach((use, index) => {
1299 28 : if (use.block) {
1300 28 : const name = teardown_block_name(use);
1301 28 : let location = use.location;
1302 :
1303 : /* Don't show mount points used internally by Cockpit.
1304 : * It's fine to tear them down, but we don't want people
1305 : * to start worrying about them.
1306 : */
1307 22 : if (location && location.startsWith(BTRFS_TOOL_MOUNT_PATH))
1308 28 : return;
1309 :
1310 21 : if (use.usage == "mounted") {
1311 21 : location = client.strip_mount_point_prefix(location);
1312 21 : if (location === false)
1313 0 : location = _("(Not part of target)");
1314 21 : }
1315 28 : rows.push({
1316 28 : columns: [name,
1317 10 : location || "-",
1318 0 : use.actions.length ? use.actions.join(", ") : "-",
1319 28 : {
1320 28 : title: <UsersPopover users={use.users || []} />,
1321 28 : props: { className: "pf-v6-u-text-align-right" }
1322 28 : }
1323 28 : ]
1324 28 : });
1325 28 : }
1326 28 : });
1327 :
1328 29 : if (rows.length == 0)
1329 3 : return null;
1330 :
1331 28 : return (
1332 28 : <div className="modal-footer-teardown">
1333 28 : <p>{_("These changes will be made:")}</p>
1334 28 : <ListingTable variant='compact'
1335 28 : columns={[
1336 28 : { title: _("Device") },
1337 28 : { title: _("Location") },
1338 28 : { title: _("Action") },
1339 28 : { title: "" }
1340 28 : ]}
1341 28 : rows={rows} />
1342 28 : </div>);
1343 71 : };
1344 :
1345 4 : const AnacondaTeardownMessage = ({ usage }) => {
1346 4 : const rows = [];
1347 :
1348 4 : usage.forEach((use, index) => {
1349 2 : if (use.data_warning) {
1350 2 : const name = teardown_block_name(use);
1351 2 : const location = client.strip_mount_point_prefix(use.location) || use.block.IdLabel || "-";
1352 :
1353 2 : rows.push(
1354 2 : <Tr key={index}>
1355 2 : <Td className="pf-v6-u-font-weight-bold">{name}</Td>
1356 2 : <Td>{location}</Td>
1357 2 : <Td>{use.data_warning}</Td>
1358 2 : </Tr>);
1359 2 : }
1360 4 : });
1361 :
1362 2 : if (rows.length > 0) {
1363 2 : return (
1364 2 : <div className="modal-footer-teardown">
1365 2 : <HelperText>
1366 2 : <HelperTextItem variant="error">
1367 2 : {_("Important data might be deleted:")}
1368 2 : </HelperTextItem>
1369 2 : </HelperText>
1370 2 : <Table variant="compact" borders={false}><Tbody>{rows}</Tbody></Table>
1371 2 : </div>);
1372 2 : }
1373 4 : };
1374 :
1375 65 : export function teardown_danger_message(usage, expect_single_unmount) {
1376 65 : if (is_expected_unmount(usage, expect_single_unmount))
1377 20 : return stop_processes_danger_message(usage[0].users);
1378 :
1379 61 : const usage_with_users = usage.filter(u => u.users);
1380 3 : const n_processes = usage_with_users.reduce((sum, u) => sum + u.users.filter(u => u.pid).length, 0);
1381 3 : const n_services = usage_with_users.reduce((sum, u) => sum + u.users.filter(u => u.unit).length, 0);
1382 3 : if (n_processes > 0 && n_services > 0) {
1383 3 : return _("Related processes and services will be forcefully stopped.");
1384 2 : } else if (n_processes > 0) {
1385 4 : return _("Related processes will be forcefully stopped.");
1386 2 : } else if (n_services > 0) {
1387 2 : return _("Related services will be forcefully stopped.");
1388 2 : } else {
1389 63 : return null;
1390 63 : }
1391 65 : }
1392 :
1393 71 : export function init_teardown_usage(client, usage, expect_single_unmount) {
1394 71 : return {
1395 71 : title: _("Checking filesystem usage"),
1396 71 : func: async function (dlg) {
1397 71 : let have_data = false;
1398 67 : for (const u of usage) {
1399 32 : if (u.usage == "mounted") {
1400 32 : u.users = await client.find_mount_users(u.location);
1401 32 : }
1402 10 : if (client.in_anaconda_mode() && !expect_single_unmount && u.block) {
1403 10 : if (u.block.IdUsage == "filesystem" &&
1404 8 : ["xfs", "ext2", "ext3", "ext4", "btrfs", "vfat", "ntfs"].indexOf(u.block.IdType) >= 0) {
1405 8 : const empty = await cockpit.script(fsys_is_empty_sh,
1406 8 : [decode_filename(u.block.PreferredDevice)],
1407 8 : { superuser: "require", err: "message" });
1408 3 : if (empty.trim() != "yes") {
1409 3 : try {
1410 3 : const info = JSON.parse(empty);
1411 3 : u.data_warning = cockpit.format(_("$0 used, $1 total"),
1412 3 : fmt_size((info.total - info.free) * info.unit),
1413 3 : fmt_size(info.total * info.unit));
1414 2 : } catch {
1415 2 : u.data_warning = _("Device contains unrecognized data");
1416 2 : }
1417 3 : }
1418 4 : } else if (u.block.IdUsage == "crypto" && !client.blocks_cleartext[u.block.path]) {
1419 4 : u.data_warning = _("Locked encrypted device might contain data");
1420 4 : } else if (!client.blocks_ptable[u.block.path] &&
1421 3 : u.block.IdUsage && u.block.IdUsage != "raid") {
1422 3 : u.data_warning = _("Device contains unrecognized data");
1423 3 : }
1424 10 : if (u.data_warning)
1425 4 : have_data = true;
1426 10 : }
1427 67 : }
1428 :
1429 4 : if (have_data) {
1430 4 : usage.Teardown = true;
1431 4 : dlg.need_confirmation(_("I confirm I want to lose this data forever"));
1432 4 : } else if (client.in_anaconda_mode() && !expect_single_unmount) {
1433 10 : dlg.need_confirmation(null);
1434 4 : } else {
1435 65 : const msg = teardown_danger_message(usage, expect_single_unmount);
1436 65 : if (msg)
1437 7 : dlg.add_danger(msg);
1438 65 : }
1439 71 : dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount));
1440 71 : }
1441 71 : };
1442 71 : }
1443 :
1444 19 : export const StopProcessesMessage = ({ mount_point, users }) => {
1445 19 : if (!users || users.length == 0)
1446 18 : return null;
1447 :
1448 3 : const process_rows = users.filter(u => u.pid).map(u => {
1449 3 : return {
1450 3 : columns: [
1451 3 : u.pid,
1452 3 : { title: u.cmd.substring(0, 100), props: { modifier: "breakWord" } },
1453 0 : u.user || "-",
1454 3 : { title: format_delay(-u.since * 1000), props: { modifier: "nowrap" } }
1455 3 : ]
1456 3 : };
1457 3 : });
1458 :
1459 3 : const service_rows = users.filter(u => u.unit).map(u => {
1460 2 : return {
1461 2 : columns: [
1462 2 : { title: u.unit.replace(/\.service$/, ""), props: { modifier: "breakWord" } },
1463 2 : { title: u.cmd.substring(0, 100), props: { modifier: "breakWord" } },
1464 0 : { title: u.desc || "", props: { modifier: "breakWord" } },
1465 2 : { title: format_delay(-u.since * 1000), props: { modifier: "nowrap" } }
1466 2 : ]
1467 2 : };
1468 2 : });
1469 :
1470 : // If both tables are shown, we press the columns into a uniform
1471 : // width to reduce the visual mess.
1472 0 : const colprops = (process_rows.length > 0 && service_rows.length > 0) ? { width: 25 } : { };
1473 :
1474 19 : return (
1475 19 : <div className="modal-footer-teardown">
1476 19 : { process_rows.length > 0
1477 3 : ? <>
1478 3 : <p>{fmt_to_fragments(_("The mount point $0 is in use by these processes:"), <b>{mount_point}</b>)}</p>
1479 3 : <ListingTable variant='compact'
1480 3 : columns={
1481 3 : [
1482 3 : { title: _("PID"), props: colprops },
1483 3 : { title: _("Command"), props: colprops },
1484 3 : { title: _("User"), props: colprops },
1485 3 : { title: _("Started"), props: colprops }
1486 3 : ]
1487 : }
1488 3 : rows={process_rows} />
1489 3 : </>
1490 0 : : null
1491 : }
1492 3 : { process_rows.length > 0 && service_rows.length > 0
1493 2 : ? <br />
1494 1 : : null
1495 : }
1496 19 : { service_rows.length > 0
1497 2 : ? <>
1498 2 : <p>{fmt_to_fragments(_("The mount point $0 is in use by these services:"), <b>{mount_point}</b>)}</p>
1499 2 : <ListingTable variant='compact'
1500 2 : columns={
1501 2 : [
1502 2 : { title: _("Service"), props: colprops },
1503 2 : { title: _("Command"), props: colprops },
1504 2 : { title: _("Description"), props: colprops },
1505 2 : { title: _("Started"), props: colprops }
1506 2 : ]
1507 : }
1508 2 : rows={service_rows} />
1509 2 : </>
1510 1 : : null
1511 : }
1512 19 : </div>);
1513 19 : };
1514 :
1515 19 : export const stop_processes_danger_message = (users) => {
1516 3 : const n_processes = users.filter(u => u.pid).length;
1517 3 : const n_services = users.filter(u => u.unit).length;
1518 :
1519 3 : if (n_processes > 0 && n_services > 0)
1520 2 : return _("The listed processes and services will be forcefully stopped.");
1521 19 : else if (n_processes > 0)
1522 0 : return _("The listed processes will be forcefully stopped.");
1523 18 : else if (n_services > 0)
1524 0 : return _("The listed services will be forcefully stopped.");
1525 : else
1526 18 : return null;
1527 19 : };
|