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