LCOV - code coverage report
Current view: top level - pkg/storaged - dialog.jsx Coverage Total Hit
Test: cockpit Lines: 93.7 % 1014 950
Test Date: 2026-07-17 14:32:59

            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 : };
        

Generated by: LCOV version 2.0-1