LCOV - code coverage report
Current view: top level - pkg/playground - dialog.tsx Coverage Total Hit
Test: cockpit Lines: 98.9 % 458 453
Test Date: 2026-06-01 17:00:21

            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            1 : import React, { useState, useReducer } from "react";
       8            1 : import { createRoot } from 'react-dom/client';
       9            1 : 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 'cockpit-dark-theme'; // once per page
      40              : import 'page.scss';
      41              : 
      42            1 : function List<T>({
      43            1 :     label,
      44            1 :     field,
      45            1 :     Component,
      46            1 :     init,
      47            1 : } : {
      48              :     label: string
      49              :     field: DialogField<T[]>,
      50              :     Component: ({ field } : { field: DialogField<T> }) => React.ReactNode,
      51              :     init: T,
      52            1 : }) {
      53            1 :     return (
      54            1 :         <FormGroup label={label} id={field.id()}>
      55            1 :             { field.map((f, i) => (
      56            1 :                 <Split key={i}>
      57            1 :                     <SplitItem isFilled>
      58            1 :                         <Component field={f} />
      59            1 :                     </SplitItem>
      60            1 :                     <SplitItem>
      61            1 :                         <Button
      62            1 :                             id={f.id("remove")}
      63            1 :                             variant="link"
      64            1 :                             onClick={() => field.remove(i)}
      65            1 :                         >
      66              :                             Remove
      67            1 :                         </Button>
      68            1 :                     </SplitItem>
      69            1 :                 </Split>
      70            1 :             ))}
      71            1 :             <DialogHelperText field={field} />
      72            1 :             <Button
      73            1 :                 id={field.id("add")}
      74            1 :                 variant="link"
      75            1 :                 onClick={() => field.add(init)}
      76            1 :             >
      77              :                 Add
      78            1 :             </Button>
      79            1 :         </FormGroup>
      80              :     );
      81            1 : }
      82              : 
      83            1 : const StringList = ({
      84            1 :     field,
      85            1 :     label,
      86            1 : } : {
      87              :     field: DialogField<string[]>,
      88              :     label: string
      89            1 : }) => {
      90            1 :     return (
      91            1 :         <List
      92            1 :             label={label}
      93            1 :             field={field}
      94            1 :             Component={DialogTextInput}
      95            1 :             init=""
      96            1 :         />
      97              :     );
      98            1 : };
      99              : 
     100              : interface Name {
     101              :     name: string;
     102              :     _length_cache: Record<string, number>;
     103              : }
     104              : 
     105            1 : const NameInput = ({
     106            1 :     field,
     107            1 : } : {
     108              :     field: DialogField<Name>,
     109            1 : }) => {
     110            1 :     return <DialogTextInput field={field.sub("name")} />;
     111            1 : };
     112              : 
     113            1 : function validate_Name(field: DialogField<Name>, countAsyncValidation: () => void) {
     114            1 :     const { _length_cache } = field.get();
     115            1 :     field.sub("name").validate_async(1000, async n => {
     116            1 :         await async_sleep(2000);
     117            1 :         countAsyncValidation();
     118            1 :         _length_cache[n] = n.length;
     119            1 :         if (n.length % 2)
     120            1 :             return "Must have even number of characters";
     121            1 :     });
     122            1 : }
     123              : 
     124            1 : const NameList = ({
     125            1 :     field,
     126            1 :     label,
     127            1 : } : {
     128              :     field: DialogField<Name[]>,
     129              :     label: string
     130            1 : }) => {
     131            1 :     return (
     132            1 :         <List
     133            1 :             label={label}
     134            1 :             field={field}
     135            1 :             Component={NameInput}
     136            1 :             init={{ name: "", _length_cache: { } }}
     137            1 :         />
     138              :     );
     139            1 : };
     140              : 
     141            1 : const OptionalTextInput = ({
     142            1 :     field_label,
     143            1 :     checkbox_label,
     144            1 :     field,
     145            1 : } : {
     146              :     field_label: string,
     147              :     checkbox_label: string;
     148              :     field: DialogField<false | string>,
     149            1 : }) => {
     150            1 :     const val = field.get();
     151            1 :     let body;
     152              : 
     153            1 :     if (val === false) {
     154            1 :         body = (
     155            1 :             <Checkbox
     156            1 :                 id={field.id("checkbox")}
     157            1 :                 isChecked={false}
     158            1 :                 label={checkbox_label}
     159            1 :                 onChange={() => field.set("")}
     160            1 :             />
     161              :         );
     162            1 :     } else {
     163            1 :         body = (
     164            1 :             <>
     165            1 :                 <Checkbox
     166            1 :                     id={field.id("checkbox")}
     167            1 :                     isChecked
     168            1 :                     label={checkbox_label}
     169            1 :                     onChange={() => field.set(false)}
     170            1 :                 />
     171            1 :                 <DialogTextInput field={field.at(val)} />
     172            1 :             </>
     173              :         );
     174            1 :     }
     175              : 
     176            1 :     return (
     177            1 :         <FormGroup
     178            1 :             label={field_label}
     179              :         >
     180            1 :             {body}
     181            1 :             <DialogHelperText field={field} />
     182            1 :         </FormGroup>
     183              :     );
     184            1 : };
     185              : 
     186            1 : function async_sleep(n: number) {
     187            1 :     return new Promise(resolve => {
     188            1 :         window.setTimeout(resolve, n);
     189            1 :     });
     190            1 : }
     191              : 
     192              : interface Color {
     193              :     name: string,
     194              :     red: number,
     195              :     green: number,
     196              :     blue: number,
     197              : }
     198              : 
     199            1 : const colors: Color[] = [
     200            1 :     { name: "red", red: 1, green: 0, blue: 0 },
     201            1 :     { name: "green", red: 0, green: 1, blue: 0 },
     202            1 :     { name: "blue", red: 0, green: 0, blue: 1 },
     203            1 : ];
     204              : 
     205              : interface ExampleValues {
     206              :     flag: boolean;
     207              :     text: string;
     208              :     radio: string;
     209              :     dropdown: string;
     210              :     color: Color,
     211              :     list: string[];
     212              :     async: Name[];
     213              :     alternative: false | string;
     214              :     error: string;
     215              : }
     216              : 
     217            1 : const ExampleDialog = ({
     218            1 :     setResult,
     219            1 :     countAsyncValidation,
     220            1 : } : {
     221              :     setResult: (values: ExampleValues) => void,
     222              :     countAsyncValidation: () => void,
     223            1 : }) => {
     224            1 :     const Dialogs = useDialogs();
     225              : 
     226            1 :     const init: ExampleValues = {
     227            1 :         flag: false,
     228            1 :         text: "",
     229            1 :         radio: "one",
     230            1 :         dropdown: "one",
     231            1 :         color: colors[0],
     232            1 :         list: [],
     233            1 :         async: [],
     234            1 :         alternative: false,
     235            1 :         error: "none",
     236            1 :     };
     237              : 
     238            1 :     function validate(dlg: DialogState<ExampleValues>) {
     239            1 :         if (dlg.values.flag) {
     240            1 :             dlg.field("text").validate(v => {
     241            1 :                 if (!v)
     242            1 :                     return "Text can not be empty";
     243            1 :             });
     244            1 :         }
     245            1 :         dlg.field("list").forEach(v => {
     246            1 :             v.validate(vv => {
     247            1 :                 if (vv == ".")
     248            0 :                     return "No dots";
     249            1 :             });
     250            1 :         });
     251            1 :         dlg.field("async").forEach(v => validate_Name(v, countAsyncValidation));
     252            1 :     }
     253              : 
     254            1 :     const dlg = useDialogState(init, validate);
     255              : 
     256            1 :     async function apply(values: ExampleValues) {
     257            1 :         setResult(values);
     258              : 
     259            1 :         if (values.error == "custom") {
     260            1 :             throw new DialogError("This is a failure", <code>1234-567-98A</code>);
     261            1 :         } else if (values.error == "from") {
     262            1 :             const err = new Error("no such file or scraper");
     263            1 :             throw DialogError.fromError("Tool not found", err);
     264            1 :         } else if (values.error == "from-random") {
     265            1 :             const err = [1, 2, 3, 4];
     266            1 :             throw DialogError.fromError("Too random", err);
     267            1 :         } else if (values.error == "message") {
     268              :             // eslint-disable-next-line no-throw-literal
     269            1 :             throw { message: "segmentation fault" };
     270            1 :         } else if (values.error == "spawn") {
     271            0 :             await cockpit.spawn(["ls", "--no-such-option"], { err: "message" });
     272            0 :         } else if (values.error == "random") {
     273              :             // eslint-disable-next-line no-throw-literal
     274            1 :             throw [1, 2, 3, 4];
     275            1 :         }
     276            1 :     }
     277              : 
     278            1 :     function update_color(color: Color) {
     279            1 :         dlg.field("text").set(color.name);
     280            1 :     }
     281              : 
     282            1 :     return (
     283            1 :         <Modal
     284            1 :             id="dialog"
     285            1 :             position="top"
     286            1 :             variant="medium"
     287            1 :             isOpen
     288            1 :             onClose={Dialogs.close}
     289              :         >
     290            1 :             <ModalHeader title="Demo" />
     291            1 :             <ModalBody>
     292            1 :                 <DialogErrorMessage dialog={dlg} />
     293            1 :                 <Form isHorizontal>
     294            1 :                     <DialogCheckbox
     295            1 :                         field_label="Checkbox"
     296            1 :                         checkbox_label="Enable text"
     297            1 :                         field={dlg.field("flag")}
     298            1 :                     />
     299            1 :                     <DialogTextInput
     300            1 :                         label="Text"
     301            1 :                         field={dlg.field("text")}
     302            1 :                         excuse={!dlg.values.flag && "Disabled"}
     303            1 :                         explanation="Explanation"
     304            1 :                         warning={dlg.values.text == "warn" ? "Warning" : null}
     305            1 :                     />
     306              :                     {
     307              :                         // Calling "map" on a non-array should just do nothing.
     308            0 :                         dlg.field("text").map((v, i) => <span key={i}>{v.get()}</span>)
     309              :                     }
     310            1 :                     <DialogRadioSelect
     311            1 :                         label="Radio"
     312            1 :                         field={dlg.field("radio")}
     313            1 :                         options={
     314            1 :                             [
     315            1 :                                 {
     316            1 :                                     value: "one",
     317            1 :                                     label: "Eins",
     318            1 :                                     explanation: "One explanation"
     319            1 :                                 },
     320            1 :                                 {
     321            1 :                                     value: "two",
     322            1 :                                     label: "Zwei",
     323            1 :                                     explanation: "Two explanation",
     324            1 :                                     excuse: "disabled",
     325            1 :                                 },
     326            1 :                                 {
     327            1 :                                     value: "three",
     328            1 :                                     label: "Drei",
     329            1 :                                 },
     330            1 :                             ]
     331              :                         }
     332            1 :                     />
     333            1 :                     <DialogDropdownSelect
     334            1 :                         label="Dropdown"
     335            1 :                         field={dlg.field("dropdown")}
     336            1 :                         options={
     337            1 :                             [
     338            1 :                                 { value: "one", label: "Eins" },
     339            1 :                                 { value: "two", label: "Zwei" },
     340            1 :                                 { value: "three", label: "Drei" },
     341            1 :                             ]
     342              :                         }
     343            1 :                         warning={dlg.field("dropdown").get() == "two" ? "There is a discount if you buy three." : null}
     344            1 :                     />
     345            1 :                     <DialogDropdownSelectObject
     346            1 :                         label="DropdownObject"
     347            1 :                         field={dlg.field("color", update_color)}
     348            1 :                         options={colors}
     349            1 :                         option_label={c => c.name}
     350            1 :                     />
     351            1 :                     <StringList label="List" field={dlg.field("list")} />
     352            1 :                     <NameList label="Async" field={dlg.field("async")} />
     353            1 :                     <OptionalTextInput
     354            1 :                         field_label="Alternative"
     355            1 :                         checkbox_label="Custom value"
     356            1 :                         field={dlg.field("alternative")}
     357            1 :                     />
     358            1 :                     <DialogDropdownSelectObject
     359            1 :                         label="Error"
     360            1 :                         field={dlg.field("error")}
     361            1 :                         options={["none", "custom", "from", "from-random", "message", "spawn", "random"]}
     362            1 :                         warning={dlg.field("error").get() != "none" ? "There will be an error" : null}
     363            1 :                     />
     364            1 :                 </Form>
     365            1 :             </ModalBody>
     366            1 :             <ModalFooter>
     367            1 :                 <DialogActionButton dialog={dlg} action={apply} onClose={Dialogs.close}>
     368              :                     Apply
     369            1 :                 </DialogActionButton>
     370            1 :                 <DialogCancelButton dialog={dlg} onClose={Dialogs.close} />
     371            1 :             </ModalFooter>
     372            1 :         </Modal>
     373              :     );
     374            1 : };
     375              : 
     376            1 : const ExampleButton = () => {
     377            1 :     const Dialogs = useDialogs();
     378            1 :     const [values, setValues] = useState<ExampleValues | null>(null);
     379            1 :     const [asyncValidationsBase, setAsycountAsyncValidationsBase] = useState<number>(0);
     380            1 :     const [asyncValidations, countAsyncValidation] = useReducer(x => x + 1, 0);
     381              : 
     382            1 :     function entry(id: string, val: string) {
     383            1 :         return (
     384            1 :             <DescriptionListGroup>
     385            1 :                 <DescriptionListTerm>{id}</DescriptionListTerm>
     386            1 :                 <DescriptionListDescription id={id}>{val}</DescriptionListDescription>
     387            1 :             </DescriptionListGroup>
     388              :         );
     389            1 :     }
     390              : 
     391            1 :     return (
     392            1 :         <>
     393            1 :             <Button
     394            1 :                 id="open"
     395            1 :                 onClick={
     396            1 :                     () => {
     397            1 :                         setAsycountAsyncValidationsBase(asyncValidations);
     398            1 :                         Dialogs.show(
     399            1 :                             <ExampleDialog
     400            1 :                                 setResult={setValues}
     401            1 :                                 countAsyncValidation={countAsyncValidation}
     402            1 :                             />
     403            1 :                         );
     404            1 :                     }
     405              :                 }
     406            1 :             >
     407              :                 Open dialog
     408            1 :             </Button>
     409            1 :             { values &&
     410            1 :                 <DescriptionList isHorizontal>
     411            1 :                     { entry("flag", String(values.flag)) }
     412            1 :                     { values.flag && entry("text", values.text) }
     413            1 :                     { entry("radio", values.radio) }
     414            1 :                     { entry("dropdown", values.dropdown) }
     415            1 :                     { entry("color", values.color.red + "/" + values.color.green + "/" + values.color.blue) }
     416            1 :                     { entry("list", values.list.join("/")) }
     417            1 :                     { entry("async", values.async.map(n => n.name + ":" + String(n._length_cache[n.name])).join("/")) }
     418            1 :                     { entry("asyncVals", String(asyncValidations - asyncValidationsBase)) }
     419            1 :                     { entry("alternative", JSON.stringify(values.alternative)) }
     420            1 :                 </DescriptionList>
     421              :             }
     422            1 :         </>
     423              :     );
     424            1 : };
     425              : 
     426              : interface ExampleWithInitFuncValues {
     427              :     text: string;
     428              :     text2: string;
     429              : }
     430              : 
     431            1 : const ExampleDialogWithInitFunc = () => {
     432            1 :     const Dialogs = useDialogs();
     433              : 
     434            1 :     function init(): ExampleWithInitFuncValues {
     435            1 :         return {
     436            1 :             text: "foo",
     437            1 :             text2: "bar",
     438            1 :         };
     439            1 :     }
     440              : 
     441            1 :     function validate(dlg: DialogState<ExampleWithInitFuncValues>) {
     442            1 :         dlg.top().validate(v => {
     443            1 :             if (v.text == "foo" && v.text2 != "bar") {
     444            1 :                 return {
     445            1 :                     text: "No foo without bar",
     446            1 :                 };
     447            1 :             }
     448            1 :             if (v.text2 == "bar" && v.text != "foo") {
     449            1 :                 return {
     450            1 :                     text2: { "": "No bar without foo" },
     451            1 :                 };
     452            1 :             }
     453            1 :         });
     454            1 :     }
     455              : 
     456            1 :     const dlg = useDialogState(init, validate);
     457              : 
     458            1 :     return (
     459            1 :         <Modal
     460            1 :             id="dialog"
     461            1 :             position="top"
     462            1 :             variant="medium"
     463            1 :             isOpen
     464            1 :             onClose={Dialogs.close}
     465              :         >
     466            1 :             <ModalHeader title="Demo" />
     467            1 :             <ModalBody>
     468            1 :                 <DialogErrorMessage dialog={dlg} />
     469            1 :                 <Form isHorizontal>
     470            1 :                     <DialogTextInput
     471            1 :                         label="Text"
     472            1 :                         field={dlg.field("text")}
     473            1 :                     />
     474            1 :                     <DialogTextInput
     475            1 :                         label="Text 2"
     476            1 :                         field={dlg.field("text2")}
     477            1 :                     />
     478            1 :                 </Form>
     479            1 :             </ModalBody>
     480            1 :             <ModalFooter>
     481            0 :                 <DialogActionButton dialog={dlg} action={async () => {}} onClose={Dialogs.close}>
     482              :                     Apply
     483            1 :                 </DialogActionButton>
     484            1 :                 <DialogCancelButton dialog={dlg} onClose={Dialogs.close} />
     485            1 :             </ModalFooter>
     486            1 :         </Modal>
     487              :     );
     488            1 : };
     489              : 
     490              : interface AsyncExampleValues {
     491              :     text: string;
     492              : }
     493              : 
     494            1 : const AsyncExampleDialog = ({
     495            1 :     throwError = 0,
     496            1 : } : {
     497              :     throwError?: number,
     498            1 : }) => {
     499            1 :     const Dialogs = useDialogs();
     500              : 
     501            1 :     async function init(): Promise<AsyncExampleValues> {
     502            1 :         if (throwError == 1)
     503            1 :             throw new Error("can't get the thing");
     504            1 :         else if (throwError == 2)
     505            1 :             throw new DialogError("Getting the thing failed", <i>can't get it</i>);
     506              : 
     507            1 :         await async_sleep(500);
     508            1 :         return {
     509            1 :             text: "",
     510            1 :         };
     511            1 :     }
     512              : 
     513            1 :     function validate(dlg: DialogState<AsyncExampleValues>) {
     514            1 :         dlg.field("text").validate_async(0, async () => {
     515            1 :             throw Error("upps");
     516            1 :         });
     517            1 :     }
     518              : 
     519            1 :     const dlg = useDialogState_async(init, validate);
     520              : 
     521            1 :     async function apply() {
     522            1 :         await async_sleep(1000);
     523            1 :         Dialogs.close();
     524            1 :     }
     525              : 
     526            1 :     function update_top(values: AsyncExampleValues) {
     527            1 :         console.log("TOP", JSON.stringify(values));
     528            1 :     }
     529              : 
     530            1 :     let body;
     531            1 :     if (!dlg) {
     532            1 :         body = (
     533            1 :             <Bullseye>
     534            1 :                 <Spinner />
     535            1 :             </Bullseye>
     536              :         );
     537            1 :     } else if (dlg instanceof DialogError) {
     538            1 :         body = null;
     539            1 :     } else if (dlg instanceof DialogState) {
     540            1 :         const vals = dlg.top(update_top);
     541            1 :         body = (
     542            1 :             <Form isHorizontal>
     543            1 :                 <DialogTextInput label="Text" field={vals.sub("text")} />
     544            1 :             </Form>
     545              :         );
     546            1 :     }
     547              : 
     548            1 :     return (
     549            1 :         <Modal
     550            1 :             id="dialog"
     551            1 :             position="top"
     552            1 :             variant="medium"
     553            1 :             isOpen
     554            1 :             onClose={Dialogs.close}
     555              :         >
     556            1 :             <ModalHeader title="Async Demo" />
     557            1 :             <ModalBody>
     558            1 :                 <DialogErrorMessage dialog={dlg} />
     559            1 :                 { body }
     560            1 :             </ModalBody>
     561            1 :             <ModalFooter>
     562            1 :                 <DialogActionButton dialog={dlg} action={apply}>
     563              :                     Apply
     564            1 :                 </DialogActionButton>
     565            1 :                 <DialogCancelButton dialog={dlg} onClose={Dialogs.close} />
     566            1 :             </ModalFooter>
     567            1 :         </Modal>
     568              :     );
     569            1 : };
     570              : 
     571            1 : const SimpleExampleButtons = () => {
     572            1 :     const Dialogs = useDialogs();
     573              : 
     574            1 :     return (
     575            1 :         <>
     576            1 :             <Button
     577            1 :                 id="open-with-func"
     578            1 :                 onClick={() => Dialogs.show(<ExampleDialogWithInitFunc />)}
     579            1 :             >
     580              :                 Open init-func dialog
     581            1 :             </Button>
     582            1 :             <Button
     583            1 :                 id="open-async"
     584            1 :                 onClick={() => Dialogs.show(<AsyncExampleDialog />)}
     585            1 :             >
     586              :                 Open async dialog
     587            1 :             </Button>
     588            1 :             <Button
     589            1 :                 id="open-error"
     590            1 :                 onClick={() => Dialogs.show(<AsyncExampleDialog throwError={1} />)}
     591            1 :             >
     592              :                 Open Error dialog
     593            1 :             </Button>
     594            1 :             <Button
     595            1 :                 id="open-dialog-error"
     596            1 :                 onClick={() => Dialogs.show(<AsyncExampleDialog throwError={2} />)}
     597            1 :             >
     598              :                 Open DialogError dialog
     599            1 :             </Button>
     600            1 :         </>
     601              :     );
     602            1 : };
     603              : 
     604            1 : const Demo = () => {
     605            1 :     return (
     606            1 :         <WithDialogs>
     607            1 :             <Page isContentFilled className="no-masthead-sidebar">
     608            1 :                 <PageSection>
     609            1 :                     <ExampleButton />
     610            1 :                     <SimpleExampleButtons />
     611            1 :                 </PageSection>
     612            1 :             </Page>
     613            1 :         </WithDialogs>
     614              :     );
     615            1 : };
     616              : 
     617            1 : document.addEventListener("DOMContentLoaded", function() {
     618            1 :     window.debugging = "dialog";
     619            1 :     createRoot(document.getElementById('app')!).render(<Demo />);
     620            1 : });
        

Generated by: LCOV version 2.0-1