LCOV - code coverage report
Current view: top level - pkg/playground - dialog.tsx Coverage Total Hit
Test: cockpit Lines: 92.5 % 643 595
Test Date: 2026-07-17 07:33:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2025 Red Hat, Inc.
       3              :  *
       4              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       5              :  */
       6              : 
       7            2 : import React, { useState, useReducer, useId } from "react";
       8            2 : import { createRoot } from 'react-dom/client';
       9            2 : import cockpit from 'cockpit';
      10              : 
      11              : import '../lib/patternfly/patternfly-6-cockpit.scss';
      12              : 
      13              : import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
      14              : import { Bullseye } from "@patternfly/react-core/dist/esm/layouts/Bullseye";
      15              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      16              : import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
      17              : import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
      18              : import { Modal, ModalBody, ModalHeader, ModalFooter } from '@patternfly/react-core/dist/esm/components/Modal';
      19              : import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
      20              : import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
      21              : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner";
      22              : 
      23              : import { WithDialogs, useDialogs } from 'dialogs';
      24              : 
      25              : import {
      26              :     useDialogState, DialogState,
      27              :     useDialogState_async,
      28              :     DialogError,
      29              :     DialogErrorMessage,
      30              :     DialogField,
      31              :     DialogCheckbox,
      32              :     DialogTextInput,
      33              :     DialogRadioSelect,
      34              :     DialogDropdownSelect, DialogDropdownSelectObject,
      35              :     DialogHelperText,
      36              :     DialogActionButton, DialogCancelButton,
      37              : } from 'cockpit/dialog';
      38              : 
      39              : import { FileChooser, DialogFileChooserInput } from "cockpit/react/FileChooser";
      40              : 
      41              : import 'cockpit-dark-theme'; // once per page
      42              : import 'page.scss';
      43              : 
      44            2 : function List<T>({
      45            2 :     label,
      46            2 :     field,
      47            2 :     Component,
      48            2 :     init,
      49            2 : } : {
      50              :     label: string
      51              :     field: DialogField<T[]>,
      52              :     Component: ({ field } : { field: DialogField<T> }) => React.ReactNode,
      53              :     init: T,
      54            2 : }) {
      55            2 :     return (
      56            2 :         <FormGroup label={label} data-ouia-component-id={field.ouia_id()}>
      57            1 :             { field.map((f, i) => (
      58            1 :                 <Split key={i}>
      59            1 :                     <SplitItem isFilled>
      60            1 :                         <Component field={f} />
      61            1 :                     </SplitItem>
      62            1 :                     <SplitItem>
      63            1 :                         <Button
      64            1 :                             ouiaId={f.ouia_id("remove")}
      65            1 :                             variant="link"
      66            1 :                             onClick={() => field.remove(i)}
      67            1 :                         >
      68              :                             Remove
      69            1 :                         </Button>
      70            1 :                     </SplitItem>
      71            1 :                 </Split>
      72            2 :             ))}
      73            2 :             <DialogHelperText field={field} />
      74            2 :             <Button
      75            2 :                 ouiaId={field.ouia_id("add")}
      76            2 :                 variant="link"
      77            1 :                 onClick={() => field.add(init)}
      78            2 :             >
      79              :                 Add
      80            2 :             </Button>
      81            2 :         </FormGroup>
      82              :     );
      83            2 : }
      84              : 
      85            2 : const StringList = ({
      86            2 :     field,
      87            2 :     label,
      88            2 : } : {
      89              :     field: DialogField<string[]>,
      90              :     label: string
      91            2 : }) => {
      92            2 :     return (
      93            2 :         <List
      94            2 :             label={label}
      95            2 :             field={field}
      96            2 :             Component={DialogTextInput}
      97            2 :             init=""
      98            2 :         />
      99              :     );
     100            2 : };
     101              : 
     102              : interface Name {
     103              :     name: string;
     104              :     _length: number;
     105              : }
     106              : 
     107            1 : const NameInput = ({
     108            1 :     field,
     109            1 : } : {
     110              :     field: DialogField<Name>,
     111            1 : }) => {
     112            1 :     return <DialogTextInput field={field.sub("name")} debounce={1000} />;
     113            1 : };
     114              : 
     115            1 : function validate_Name(field: DialogField<Name>, countAsyncValidation: () => void) {
     116            1 :     field.sub("name").validate_async(async (n, signal) => {
     117            1 :         await async_sleep(2000);
     118            1 :         countAsyncValidation();
     119            1 :         if (!signal.aborted)
     120            1 :             field.sub("_length").set(n.length);
     121            1 :         if (n.length % 2)
     122            1 :             return "Must have even number of characters";
     123            1 :     });
     124            1 : }
     125              : 
     126            2 : const NameList = ({
     127            2 :     field,
     128            2 :     label,
     129            2 : } : {
     130              :     field: DialogField<Name[]>,
     131              :     label: string
     132            2 : }) => {
     133            2 :     return (
     134            2 :         <List
     135            2 :             label={label}
     136            2 :             field={field}
     137            2 :             Component={NameInput}
     138            2 :             init={{ name: "", _length: 0 }}
     139            2 :         />
     140              :     );
     141            2 : };
     142              : 
     143            2 : const OptionalTextInput = ({
     144            2 :     field_label,
     145            2 :     checkbox_label,
     146            2 :     field,
     147            2 : } : {
     148              :     field_label: string,
     149              :     checkbox_label: string;
     150              :     field: DialogField<false | string>,
     151            2 : }) => {
     152            2 :     const id = useId();
     153            2 :     const val = field.get();
     154            2 :     let body;
     155              : 
     156            2 :     if (val === false) {
     157            2 :         body = (
     158            2 :             <Checkbox
     159            2 :                 id={id}
     160            2 :                 ouiaId={field.ouia_id("checkbox")}
     161            2 :                 isChecked={false}
     162            2 :                 label={checkbox_label}
     163            1 :                 onChange={() => field.set("")}
     164            2 :             />
     165              :         );
     166            1 :     } else {
     167            1 :         body = (
     168            1 :             <>
     169            1 :                 <Checkbox
     170            1 :                     id={id}
     171            1 :                     ouiaId={field.ouia_id("checkbox")}
     172            1 :                     isChecked
     173            1 :                     label={checkbox_label}
     174            1 :                     onChange={() => field.set(false)}
     175            1 :                 />
     176            1 :                 <DialogTextInput field={field.at(val)} />
     177            1 :             </>
     178              :         );
     179            1 :     }
     180              : 
     181            2 :     return (
     182            2 :         <FormGroup
     183            2 :             label={field_label}
     184              :         >
     185            2 :             {body}
     186            2 :             <DialogHelperText field={field} />
     187            2 :         </FormGroup>
     188              :     );
     189            2 : };
     190              : 
     191            1 : function async_sleep(n: number) {
     192            1 :     return new Promise(resolve => {
     193            1 :         window.setTimeout(resolve, n);
     194            1 :     });
     195            1 : }
     196              : 
     197              : interface Color {
     198              :     name: string,
     199              :     red: number,
     200              :     green: number,
     201              :     blue: number,
     202              : }
     203              : 
     204            2 : const colors: Color[] = [
     205            2 :     { name: "red", red: 1, green: 0, blue: 0 },
     206            2 :     { name: "green", red: 0, green: 1, blue: 0 },
     207            2 :     { name: "blue", red: 0, green: 0, blue: 1 },
     208            2 : ];
     209              : 
     210              : interface ExampleValues {
     211              :     flag: boolean;
     212              :     text: string;
     213              :     text2: string;
     214              :     radio: string;
     215              :     dropdown: string;
     216              :     text3: string;
     217              :     color: Color,
     218              :     list: string[];
     219              :     async: Name[];
     220              :     alternative: false | string;
     221              :     error: string;
     222              :     allow_force: boolean;
     223              :     file: string;
     224              :     file_explanation: string;
     225              :     dir: string;
     226              : }
     227              : 
     228            2 : const ExampleDialog = ({
     229            2 :     setResult,
     230            2 :     countAsyncValidation,
     231            2 :     countAsyncUpdate,
     232            2 :     countAsyncCancel,
     233            2 : } : {
     234              :     setResult: (values: ExampleValues) => void,
     235              :     countAsyncValidation: () => void,
     236              :     countAsyncUpdate: () => void,
     237              :     countAsyncCancel: () => void,
     238            2 : }) => {
     239            2 :     const Dialogs = useDialogs();
     240              : 
     241            2 :     const init: ExampleValues = {
     242            2 :         flag: false,
     243            2 :         text: "",
     244            2 :         text2: "",
     245            2 :         radio: "one",
     246            2 :         dropdown: "one",
     247            2 :         text3: "",
     248            2 :         color: colors[0],
     249            2 :         list: [],
     250            2 :         async: [],
     251            2 :         alternative: false,
     252            2 :         error: "none",
     253            2 :         allow_force: false,
     254            2 :         file: "",
     255            2 :         file_explanation: "",
     256            2 :         dir: "",
     257            2 :     };
     258              : 
     259            1 :     function validate(dlg: DialogState<ExampleValues>) {
     260            1 :         if (dlg.values.flag) {
     261            1 :             dlg.field("text").validate(v => {
     262            1 :                 if (!v)
     263            1 :                     return "Text can not be empty";
     264            1 :             });
     265            1 :         }
     266            1 :         if (dlg.values.dropdown == "three") {
     267            1 :             dlg.field("text3").validate(v => {
     268            1 :                 if (!v)
     269            1 :                     return "Can't be empty";
     270            1 :             });
     271            1 :         }
     272            1 :         dlg.field("list").forEach(v => {
     273            1 :             v.validate(vv => {
     274            1 :                 if (vv == "magic")
     275            1 :                     dlg.field("text").set("magic");
     276            1 :                 if (vv == ".")
     277            0 :                     return "No dots";
     278            1 :             });
     279            1 :         });
     280            1 :         dlg.field("async").forEach(v => validate_Name(v, countAsyncValidation));
     281            1 :         dlg.field("file").validate(v => {
     282            0 :             if (v && v[0] != "/")
     283            0 :                 return "Must be absolute";
     284            1 :         });
     285            1 :     }
     286              : 
     287            2 :     const dlg = useDialogState(init, validate);
     288              : 
     289            1 :     async function apply(values: ExampleValues, variant: string) {
     290            1 :         setResult(values);
     291              : 
     292            1 :         if (variant == "force")
     293            1 :             return;
     294              : 
     295            1 :         if (values.error == "custom") {
     296            1 :             throw new DialogError("This is a failure", <code>1234-567-98A</code>);
     297            1 :         } else if (values.error == "from") {
     298            1 :             const err = new Error("no such file or scraper");
     299            1 :             throw DialogError.fromError("Tool not found", err);
     300            1 :         } else if (values.error == "from-random") {
     301            1 :             const err = [1, 2, 3, 4];
     302            1 :             throw DialogError.fromError("Too random", err);
     303            1 :         } else if (values.error == "message") {
     304              :             // eslint-disable-next-line no-throw-literal
     305            1 :             throw { message: "segmentation fault" };
     306            1 :         } else if (values.error == "spawn") {
     307            0 :             await cockpit.spawn(["ls", "--no-such-option"], { err: "message" });
     308            0 :         } else if (values.error == "random") {
     309              :             // eslint-disable-next-line no-throw-literal
     310            1 :             throw [1, 2, 3, 4];
     311            1 :         }
     312            1 :     }
     313              : 
     314            1 :     function color_changed() {
     315            1 :         dlg.field("color").get_async(async (val, signal) => {
     316            1 :             signal.onabort = countAsyncCancel;
     317            1 :             await async_sleep(2000);
     318            1 :             if (!signal.aborted) {
     319            1 :                 countAsyncUpdate();
     320            1 :                 dlg.field("text").set(val.name);
     321            1 :             }
     322            1 :         });
     323            1 :     }
     324              : 
     325            1 :     function dropdown_changed(val: string) {
     326            1 :         dlg.field("text2").set_async(async () => {
     327            1 :             await async_sleep(2000);
     328            1 :             return val;
     329            1 :         });
     330            1 :     }
     331              : 
     332            1 :     function file_changed(val: string) {
     333            1 :         dlg.field("file_explanation").set_async(async () => {
     334            1 :             if (val[0] == "/")
     335            1 :                 return cockpit.spawn(["file", "-b", val], { superuser: "try" });
     336              :             else
     337            1 :                 return "--";
     338            1 :         });
     339            1 :     }
     340              : 
     341            2 :     return (
     342            2 :         <Modal
     343            2 :             id="dialog"
     344            2 :             position="top"
     345            2 :             variant="medium"
     346            2 :             isOpen
     347            2 :             onClose={Dialogs.close}
     348              :         >
     349            2 :             <ModalHeader title="Demo" />
     350            2 :             <ModalBody>
     351            2 :                 <DialogErrorMessage dialog={dlg} />
     352            2 :                 <Form isHorizontal>
     353            2 :                     <DialogCheckbox
     354            2 :                         field_label="Checkbox"
     355            2 :                         checkbox_label="Enable text"
     356            2 :                         field={dlg.field("flag")}
     357            2 :                     />
     358            2 :                     <DialogTextInput
     359            2 :                         label="Text"
     360            2 :                         field={dlg.field("text")}
     361            2 :                         debounce={0}
     362            2 :                         excuse={!dlg.values.flag && "Disabled"}
     363            2 :                         explanation="Explanation"
     364            1 :                         warning={dlg.values.text == "warn" ? "Warning" : null}
     365            2 :                     />
     366            2 :                     <DialogTextInput
     367            2 :                         label="Text2"
     368            2 :                         field={dlg.field("text2")}
     369            2 :                     />
     370              :                     {
     371              :                         // Calling "map" on a non-array should just do nothing.
     372            0 :                         dlg.field("text").map((v, i) => <span key={i}>{v.get()}</span>)
     373              :                     }
     374            2 :                     <DialogRadioSelect
     375            2 :                         label="Radio"
     376            2 :                         field={dlg.field("radio")}
     377            2 :                         options={
     378            2 :                             [
     379            2 :                                 {
     380            2 :                                     value: "one",
     381            2 :                                     label: "Eins",
     382            2 :                                     explanation: "One explanation"
     383            2 :                                 },
     384            2 :                                 {
     385            2 :                                     value: "two",
     386            2 :                                     label: "Zwei",
     387            2 :                                     explanation: "Two explanation",
     388            2 :                                     excuse: "disabled",
     389            2 :                                 },
     390            2 :                                 {
     391            2 :                                     value: "three",
     392            2 :                                     label: "Drei",
     393            2 :                                 },
     394            2 :                             ]
     395              :                         }
     396            2 :                     />
     397            2 :                     <DialogDropdownSelect
     398            2 :                         label="Dropdown"
     399            2 :                         field={dlg.field("dropdown", dropdown_changed)}
     400            2 :                         options={
     401            2 :                             [
     402            2 :                                 { value: "one", label: "Eins" },
     403            2 :                                 { value: "two", label: "Zwei" },
     404            2 :                                 { value: "three", label: "Drei" },
     405            2 :                             ]
     406              :                         }
     407            1 :                         warning={dlg.field("dropdown").get() == "two" ? "There is a discount if you buy three." : null}
     408            2 :                     />
     409              :                     {
     410            2 :                         dlg.values.dropdown == "three" &&
     411            1 :                             <DialogTextInput label="Text3" field={dlg.field("text3")} debounce={1000} />
     412              :                     }
     413            2 :                     <DialogDropdownSelectObject
     414            2 :                         label="DropdownObject"
     415            2 :                         field={dlg.field("color").notify(color_changed)}
     416            2 :                         options={colors}
     417            2 :                         option_label={c => c.name}
     418            2 :                     />
     419            2 :                     <StringList label="List" field={dlg.field("list")} />
     420            2 :                     <NameList label="Async" field={dlg.field("async")} />
     421            2 :                     <OptionalTextInput
     422            2 :                         field_label="Alternative"
     423            2 :                         checkbox_label="Custom value"
     424            2 :                         field={dlg.field("alternative")}
     425            2 :                     />
     426            2 :                     <DialogDropdownSelectObject
     427            2 :                         label="Error"
     428            2 :                         field={dlg.field("error")}
     429            2 :                         options={["none", "custom", "from", "from-random", "message", "spawn", "random"]}
     430            1 :                         warning={dlg.field("error").get() != "none" ? "There will be an error unless you apply with force" : null}
     431            2 :                     />
     432            2 :                     <DialogCheckbox
     433            2 :                         field_label="Action options"
     434            2 :                         checkbox_label="Allow force"
     435            2 :                         field={dlg.field("allow_force")}
     436            2 :                     />
     437            2 :                     <DialogFileChooserInput
     438            2 :                         label="File"
     439            2 :                         field={dlg.field("file", file_changed)}
     440            2 :                         explanation={dlg.values.file_explanation}
     441            2 :                         fileChooserProps={
     442            2 :                             {
     443            2 :                                 title: "Select a file",
     444            2 :                                 superuser: "try",
     445            2 :                                 filters: [
     446            1 :                                     { label: "No dots", filter: n => !n.includes(".") },
     447            2 :                                 ],
     448            2 :                                 shortcuts: [
     449            2 :                                     { label: "Test files", path: "/var/lib/cockpittest" }
     450            2 :                                 ],
     451            2 :                                 collections: [
     452            2 :                                     {
     453            2 :                                         label: "Some files",
     454            2 :                                         emptyLabel: "Nothing there",
     455            1 :                                         list: async () => {
     456            1 :                                             return [
     457            1 :                                                 "/var/lib/cockpittest/file-chooser-test/dots.txt",
     458            1 :                                                 "/var/lib/cockpittest/file-chooser-test/foo",
     459            1 :                                             ];
     460            1 :                                         }
     461            2 :                                     }
     462            2 :                                 ]
     463            2 :                             }
     464              :                         }
     465            2 :                     />
     466            2 :                     <DialogFileChooserInput
     467            2 :                         label="Directory"
     468            2 :                         field={dlg.field("dir")}
     469            2 :                         fileChooserProps={
     470            2 :                             {
     471            2 :                                 title: "Select a directory",
     472            2 :                                 onlyDirectories: true,
     473            2 :                                 superuser: "try",
     474            2 :                                 shortcuts: [
     475            2 :                                     { label: "Test files", path: "/var/lib/cockpittest" }
     476            2 :                                 ],
     477            2 :                             }
     478              :                         }
     479            2 :                     />
     480            2 :                 </Form>
     481            2 :             </ModalBody>
     482            2 :             <ModalFooter>
     483            2 :                 <DialogActionButton
     484            2 :                     dialog={dlg}
     485            1 :                     action={values => apply(values, "main")}
     486            2 :                     onClose={Dialogs.close}
     487            2 :                 >
     488              :                     Apply
     489            2 :                 </DialogActionButton>
     490            2 :                 <DialogActionButton
     491            2 :                     dialog={dlg}
     492            1 :                     action={values => apply(values, "force")}
     493            2 :                     ouiaId="apply-force"
     494            2 :                     variant="danger"
     495            2 :                     onClose={Dialogs.close}
     496            1 :                     excuse={dlg.values.allow_force ? undefined : "Force not allowed"}
     497            2 :                 >
     498              :                     Apply with force
     499            2 :                 </DialogActionButton>
     500            2 :                 <DialogCancelButton dialog={dlg} onClose={Dialogs.close}>
     501              :                     CANCEL!
     502            2 :                 </DialogCancelButton>
     503            2 :             </ModalFooter>
     504            2 :         </Modal>
     505              :     );
     506            2 : };
     507              : 
     508            2 : const ExampleButton = () => {
     509            2 :     const Dialogs = useDialogs();
     510            2 :     const [values, setValues] = useState<ExampleValues | null>(null);
     511            2 :     const [asyncValidationsBase, setAsyncValidationsBase] = useState<number>(0);
     512            1 :     const [asyncValidations, countAsyncValidation] = useReducer(x => x + 1, 0);
     513            2 :     const [asyncUpdatesBase, setAsyncUpdatesBase] = useState<number>(0);
     514            1 :     const [asyncUpdates, countAsyncUpdate] = useReducer(x => x + 1, 0);
     515            2 :     const [asyncCancelsBase, setAsyncCancelsBase] = useState<number>(0);
     516            1 :     const [asyncCancels, countAsyncCancel] = useReducer(x => x + 1, 0);
     517              : 
     518            1 :     function entry(id: string, val: string) {
     519            1 :         return (
     520            1 :             <DescriptionListGroup>
     521            1 :                 <DescriptionListTerm>{id}</DescriptionListTerm>
     522            1 :                 <DescriptionListDescription id={id}>{val}</DescriptionListDescription>
     523            1 :             </DescriptionListGroup>
     524              :         );
     525            1 :     }
     526              : 
     527            2 :     return (
     528            2 :         <>
     529            2 :             <Button
     530            2 :                 id="open"
     531            2 :                 onClick={
     532            2 :                     () => {
     533            2 :                         setAsyncValidationsBase(asyncValidations);
     534            2 :                         setAsyncUpdatesBase(asyncUpdates);
     535            2 :                         setAsyncCancelsBase(asyncCancels);
     536            2 :                         Dialogs.show(
     537            2 :                             <ExampleDialog
     538            2 :                                 setResult={setValues}
     539            2 :                                 countAsyncValidation={countAsyncValidation}
     540            2 :                                 countAsyncUpdate={countAsyncUpdate}
     541            2 :                                 countAsyncCancel={countAsyncCancel}
     542            2 :                             />
     543            2 :                         );
     544            2 :                     }
     545              :                 }
     546            2 :             >
     547              :                 Open dialog
     548            2 :             </Button>
     549            2 :             { values &&
     550            1 :                 <DescriptionList isHorizontal>
     551            1 :                     { entry("flag", String(values.flag)) }
     552            1 :                     { values.flag && entry("text", values.text) }
     553            1 :                     { entry("text2", values.text2) }
     554            1 :                     { entry("radio", values.radio) }
     555            1 :                     { entry("dropdown", values.dropdown) }
     556            1 :                     { entry("color", values.color.red + "/" + values.color.green + "/" + values.color.blue) }
     557            1 :                     { entry("list", values.list.join("/")) }
     558            1 :                     { entry("async", values.async.map(n => n.name + ":" + String(n._length)).join("/")) }
     559            1 :                     { entry("asyncVals", String(asyncValidations - asyncValidationsBase)) }
     560            1 :                     { entry("asyncUps", String(asyncUpdates - asyncUpdatesBase)) }
     561            1 :                     { entry("asyncCancels", String(asyncCancels - asyncCancelsBase)) }
     562            1 :                     { entry("alternative", JSON.stringify(values.alternative)) }
     563            1 :                 </DescriptionList>
     564              :             }
     565            2 :         </>
     566              :     );
     567            2 : };
     568              : 
     569              : interface ExampleWithInitFuncValues {
     570              :     text: string;
     571              :     text2: string;
     572              : }
     573              : 
     574            1 : const ExampleDialogWithInitFunc = () => {
     575            1 :     const Dialogs = useDialogs();
     576              : 
     577            1 :     function init(): ExampleWithInitFuncValues {
     578            1 :         return {
     579            1 :             text: "foo",
     580            1 :             text2: "bar",
     581            1 :         };
     582            1 :     }
     583              : 
     584            1 :     function validate(dlg: DialogState<ExampleWithInitFuncValues>) {
     585            1 :         dlg.top().validate(v => {
     586            1 :             if (v.text == "foo" && v.text2 != "bar") {
     587            1 :                 return {
     588            1 :                     text: "No foo without bar",
     589            1 :                 };
     590            1 :             }
     591            1 :             if (v.text2 == "bar" && v.text != "foo") {
     592            1 :                 return {
     593            1 :                     text2: { "": "No bar without foo" },
     594            1 :                 };
     595            1 :             }
     596            1 :         });
     597            1 :     }
     598              : 
     599            1 :     const dlg = useDialogState(init, validate);
     600              : 
     601            1 :     return (
     602            1 :         <Modal
     603            1 :             id="dialog"
     604            1 :             position="top"
     605            1 :             variant="medium"
     606            1 :             isOpen
     607            1 :             onClose={Dialogs.close}
     608              :         >
     609            1 :             <ModalHeader title="Demo" />
     610            1 :             <ModalBody>
     611            1 :                 <DialogErrorMessage dialog={dlg} />
     612            1 :                 <Form isHorizontal>
     613            1 :                     <DialogTextInput
     614            1 :                         label="Text"
     615            1 :                         field={dlg.field("text")}
     616            1 :                     />
     617            1 :                     <DialogTextInput
     618            1 :                         label="Text 2"
     619            1 :                         field={dlg.field("text2")}
     620            1 :                     />
     621            1 :                 </Form>
     622            1 :             </ModalBody>
     623            1 :             <ModalFooter>
     624            0 :                 <DialogActionButton dialog={dlg} action={async () => {}} onClose={Dialogs.close}>
     625              :                     Apply
     626            1 :                 </DialogActionButton>
     627            1 :                 <DialogCancelButton dialog={dlg} onClose={Dialogs.close} />
     628            1 :             </ModalFooter>
     629            1 :         </Modal>
     630              :     );
     631            1 : };
     632              : 
     633              : interface AsyncExampleValues {
     634              :     text: string;
     635              : }
     636              : 
     637            1 : const AsyncExampleDialog = ({
     638            1 :     throwError = 0,
     639            1 :     cancelCallback = null,
     640            1 : } : {
     641              :     throwError?: number,
     642              :     cancelCallback?: null | (() => void),
     643            1 : }) => {
     644            1 :     const Dialogs = useDialogs();
     645              : 
     646            1 :     async function init(): Promise<AsyncExampleValues> {
     647            1 :         if (throwError == 1)
     648            1 :             throw new Error("can't get the thing");
     649            1 :         else if (throwError == 2)
     650            1 :             throw new DialogError("Getting the thing failed", <i>can't get it</i>);
     651              : 
     652            1 :         await async_sleep(500);
     653            1 :         return {
     654            1 :             text: "",
     655            1 :         };
     656            1 :     }
     657              : 
     658            1 :     function validate(dlg: DialogState<AsyncExampleValues>) {
     659            1 :         dlg.field("text").validate_async(async () => {
     660            1 :             throw Error("upps");
     661            1 :         });
     662            1 :     }
     663              : 
     664            1 :     const dlg = useDialogState_async(init, validate);
     665              : 
     666            1 :     async function apply() {
     667            1 :         cockpit.assert(dlg instanceof DialogState);
     668              : 
     669            1 :         dlg.set_cancel(cancelCallback);
     670              : 
     671            1 :         await async_sleep(1000);
     672            1 :         Dialogs.close();
     673            1 :     }
     674              : 
     675            1 :     function top_changed(values: AsyncExampleValues) {
     676            1 :         console.log("TOP", JSON.stringify(values));
     677            1 :     }
     678              : 
     679            1 :     let body;
     680            1 :     if (!dlg) {
     681            1 :         body = (
     682            1 :             <Bullseye>
     683            1 :                 <Spinner />
     684            1 :             </Bullseye>
     685              :         );
     686            1 :     } else if (dlg instanceof DialogError) {
     687            1 :         body = null;
     688            1 :     } else if (dlg instanceof DialogState) {
     689            1 :         const fields = dlg.top(top_changed);
     690            1 :         body = (
     691            1 :             <Form isHorizontal>
     692            1 :                 <DialogTextInput label="Text" field={fields.sub("text")} />
     693            1 :             </Form>
     694              :         );
     695            1 :     }
     696              : 
     697            1 :     return (
     698            1 :         <Modal
     699            1 :             id="dialog"
     700            1 :             position="top"
     701            1 :             variant="medium"
     702            1 :             isOpen
     703            1 :             onClose={Dialogs.close}
     704              :         >
     705            1 :             <ModalHeader title="Async Demo" />
     706            1 :             <ModalBody>
     707            1 :                 <DialogErrorMessage dialog={dlg} />
     708            1 :                 { body }
     709            1 :             </ModalBody>
     710            1 :             <ModalFooter>
     711            1 :                 <DialogActionButton dialog={dlg} action={apply}>
     712              :                     Apply
     713            1 :                 </DialogActionButton>
     714            1 :                 <DialogCancelButton dialog={dlg} onClose={Dialogs.close} />
     715            1 :             </ModalFooter>
     716            1 :         </Modal>
     717              :     );
     718            1 : };
     719              : 
     720            2 : const SimpleExampleButtons = () => {
     721            2 :     const Dialogs = useDialogs();
     722            2 :     const [cancelled, setCancelled] = useState(false);
     723              : 
     724            2 :     return (
     725            2 :         <>
     726            2 :             <Button
     727            2 :                 id="open-with-func"
     728            1 :                 onClick={() => Dialogs.show(<ExampleDialogWithInitFunc />)}
     729            2 :             >
     730              :                 Open init-func dialog
     731            2 :             </Button>
     732            2 :             <Button
     733            2 :                 id="open-async"
     734            2 :                 onClick={
     735            1 :                     () => {
     736            1 :                         setCancelled(false);
     737            1 :                         Dialogs.show(<AsyncExampleDialog cancelCallback={() => setCancelled(true)} />);
     738            1 :                     }
     739              :                 }
     740            2 :             >
     741              :                 Open async dialog
     742            2 :             </Button>
     743            2 :             <div id="cancelled">
     744            1 :                 Cancelled: {cancelled ? "yes" : "no"}
     745            2 :             </div>
     746            2 :             <Button
     747            2 :                 id="open-error"
     748            1 :                 onClick={() => Dialogs.show(<AsyncExampleDialog throwError={1} />)}
     749            2 :             >
     750              :                 Open Error dialog
     751            2 :             </Button>
     752            2 :             <Button
     753            2 :                 id="open-dialog-error"
     754            1 :                 onClick={() => Dialogs.show(<AsyncExampleDialog throwError={2} />)}
     755            2 :             >
     756              :                 Open DialogError dialog
     757            2 :             </Button>
     758            2 :         </>
     759              :     );
     760            2 : };
     761              : 
     762            2 : const FileChooserButton = () => {
     763            2 :     const Dialogs = useDialogs();
     764              : 
     765            0 :     async function loadFile(path: string) {
     766            0 :         const data = await cockpit.file(path).read();
     767            0 :         if (!data.startsWith("foo"))
     768            0 :             throw new Error("Does not start with \"foo\"");
     769            0 :     }
     770              : 
     771            2 :     return (
     772            2 :         <Button
     773            2 :             id="open-file-chooser"
     774            2 :             onClick={
     775            0 :                 () => Dialogs.show(
     776            0 :                     <FileChooser
     777            0 :                         title={"Select a file that starts with \"foo\""}
     778            0 :                         actionLabel="Load"
     779            0 :                         action={loadFile}
     780            0 :                         filters={
     781            0 :                             [
     782            0 :                                 {
     783            0 :                                     label: "TXT files",
     784            0 :                                     filter: (name, type) => type == "reg" && !!name.match("\\.txt$")
     785            0 :                                 },
     786            0 :                             ]
     787              :                         }
     788            0 :                         shortcuts={
     789            0 :                             async () => {
     790            0 :                                 async_sleep(500);
     791            0 :                                 return [
     792            0 :                                     { label: "Test files", path: "/var/lib/cockpittest" }
     793            0 :                                 ];
     794            0 :                             }
     795              :                         }
     796            0 :                         collections={
     797            0 :                             async () => {
     798            0 :                                 return [
     799            0 :                                     {
     800            0 :                                         label: "Some TXT files",
     801            0 :                                         emptyLabel: "Nothing there",
     802            0 :                                         list: async () => {
     803            0 :                                             return [
     804            0 :                                                 "/var/lib/cockpittest/file-chooser-test/text/foo.txt",
     805            0 :                                                 "/var/lib/cockpittest/file-chooser-test/no-such-file.txt"
     806            0 :                                             ];
     807            0 :                                         }
     808            0 :                                     }
     809            0 :                                 ];
     810            0 :                             }
     811              :                         }
     812            0 :                     />
     813            0 :                 )
     814              :             }
     815            2 :         >
     816              :             Open FileChooser
     817            2 :         </Button>
     818              :     );
     819            2 : };
     820              : 
     821            2 : const Demo = () => {
     822            2 :     return (
     823            2 :         <WithDialogs>
     824            2 :             <Page isContentFilled className="no-masthead-sidebar">
     825            2 :                 <PageSection>
     826            2 :                     <ExampleButton />
     827            2 :                     <SimpleExampleButtons />
     828            2 :                     <FileChooserButton />
     829            2 :                 </PageSection>
     830            2 :             </Page>
     831            2 :         </WithDialogs>
     832              :     );
     833            2 : };
     834              : 
     835            2 : document.addEventListener("DOMContentLoaded", function() {
     836            2 :     window.debugging = "dialog";
     837            2 :     createRoot(document.getElementById('app')!).render(<Demo />);
     838            2 : });
        

Generated by: LCOV version 2.0-1