Line data Source code
1 : /*
2 : * Copyright (C) 2024 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 2 : import cockpit from "cockpit";
7 2 : import React, { useRef, useState } from "react";
8 :
9 : import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
10 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
11 : import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
12 : import { Progress } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
13 : import { RhMicronsCloseIcon, UploadIcon } from "@patternfly/react-icons";
14 :
15 : import { FileAutoComplete } from "cockpit-components-file-autocomplete.jsx";
16 : import { upload } from "cockpit-upload-helper";
17 :
18 2 : const _ = cockpit.gettext;
19 :
20 2 : export const UploadDemo = () => {
21 2 : const ref = useRef<HTMLInputElement>(null);
22 2 : const [files, setFiles] = useState<{[name: string]: {file: File, progress: number, cancel:() => void}}>({});
23 2 : const [alert, setAlert] = useState<{variant: "warning" | "danger", title: string, message: string} | null>(null);
24 2 : const [dest, setDest] = useState("/home/admin/");
25 2 : let next_progress = 0;
26 :
27 0 : const handleClick = () => {
28 0 : if (ref.current) {
29 0 : ref.current.click();
30 0 : }
31 0 : };
32 :
33 0 : const onUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
34 0 : cockpit.assert(event.target.files, "not an <input type='file'>?");
35 0 : setAlert(null);
36 0 : await Promise.allSettled(Array.from(event.target.files).map(async (file: File) => {
37 0 : const destination = `${dest}${file.name}`;
38 0 : const abort = new AbortController();
39 :
40 0 : setFiles(oldFiles => {
41 0 : return {
42 0 : [file.name]: { file, progress: 0, cancel: () => abort.abort() },
43 0 : ...oldFiles,
44 0 : };
45 0 : });
46 :
47 0 : try {
48 0 : await upload(destination, file, (progress) => {
49 0 : const now = performance.now();
50 0 : if (now < next_progress)
51 0 : return;
52 0 : next_progress = now + 200; // only rerender every 200ms
53 0 : setFiles(oldFiles => {
54 0 : const oldFile = oldFiles[file.name];
55 0 : return {
56 0 : ...oldFiles,
57 0 : [file.name]: { ...oldFile, progress },
58 0 : };
59 0 : });
60 0 : }, abort.signal);
61 0 : } catch (exc) {
62 0 : cockpit.assert(exc instanceof Error, "Unknown exception type");
63 0 : if (exc instanceof DOMException && exc.name == 'AbortError') {
64 0 : setAlert({ variant: "warning", title: 'Aborted', message: '' });
65 0 : } else {
66 0 : setAlert({ variant: "danger", title: 'Upload Error', message: exc.message });
67 0 : }
68 0 : } finally {
69 0 : setFiles(oldFiles => {
70 0 : const copy = { ...oldFiles };
71 0 : delete copy[file.name];
72 0 : return copy;
73 0 : });
74 0 : }
75 0 : }));
76 :
77 : // Reset input field in the case a download was cancelled and has to be re-uploaded
78 : // https://stackoverflow.com/questions/26634616/filereader-upload-same-file-again-not-working
79 0 : event.target.value = "";
80 0 : };
81 :
82 2 : return (
83 2 : <>
84 2 : <Flex direction={{ default: "column" }}>
85 2 : <FileAutoComplete value={dest} onChange={setDest} />
86 2 : <Button
87 2 : id="upload-file-btn"
88 2 : variant="secondary"
89 2 : icon={<UploadIcon />}
90 2 : isDisabled={Object.keys(files).length !== 0}
91 2 : isLoading={Object.keys(files).length !== 0}
92 2 : onClick={handleClick}
93 : >
94 2 : {_("Upload")}
95 2 : </Button>
96 2 : <input
97 2 : ref={ref} type="file"
98 2 : hidden multiple onChange={onUpload}
99 2 : />
100 2 : </Flex>
101 2 : {alert !== null &&
102 0 : <Alert variant={alert.variant}
103 0 : title={alert.title}
104 0 : timeout={3000}
105 0 : actionClose={<AlertActionCloseButton onClose={() => setAlert(null)} />}
106 : >
107 0 : <p>{alert.message}</p>
108 0 : </Alert>
109 : }
110 0 : {Object.keys(files).map((key, index) => {
111 0 : const file = files[key];
112 0 : return (
113 0 : <React.Fragment key={index}>
114 0 : <Progress className={`upload-progress-${index}`} key={file.file.name} value={file.progress} title={file.file.name} max={file.file.size} />
115 0 : <Button className={`cancel-button-${index}`} icon={<RhMicronsCloseIcon />} onClick={file.cancel} />
116 0 : </React.Fragment>
117 : );
118 0 : })}
119 2 : </>
120 : );
121 2 : };
|