LCOV - code coverage report
Current view: top level - pkg/playground - react-demo-file-upload.tsx Coverage Total Hit
Test: cockpit Lines: 32.6 % 92 30
Test Date: 2026-06-16 14:09:37

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

Generated by: LCOV version 2.0-1