Philipp Heckel 3 lat temu
rodzic
commit
aabae53e5d
2 zmienionych plików z 115 dodań i 33 usunięć
  1. 39 0
      web/src/app/Api.js
  2. 76 33
      web/src/components/SendDialog.js

+ 39 - 0
web/src/app/Api.js

@@ -1,4 +1,6 @@
 import {
 import {
+    basicAuth,
+    encodeBase64,
     fetchLinesIterator,
     fetchLinesIterator,
     maybeWithBasicAuth,
     maybeWithBasicAuth,
     topicShortUrl,
     topicShortUrl,
@@ -42,6 +44,43 @@ class Api {
         });
         });
     }
     }
 
 
+    publishXHR(baseUrl, topic, body, headers, onProgress) {
+        const url = topicUrl(baseUrl, topic);
+        const xhr = new XMLHttpRequest();
+
+        console.log(`[Api] Publishing message to ${url}`);
+        const send = new Promise(function (resolve, reject) {
+            xhr.open("PUT", url);
+            xhr.addEventListener('readystatechange', (ev) => {
+                if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
+                    console.log(`[Api] Publish successful`, ev);
+                    resolve(xhr.response);
+                } else if (xhr.readyState === 4) {
+                    console.log(`[Api] Publish failed (1)`, ev);
+                    xhr.abort();
+                    reject(ev);
+                }
+            })
+            xhr.onerror = (ev) => {
+                console.log(`[Api] Publish failed (2)`, ev);
+                reject(ev);
+            };
+            xhr.upload.addEventListener("progress", onProgress);
+            if (body.type) {
+                xhr.overrideMimeType(body.type);
+            }
+            for (const [key, value] of Object.entries(headers)) {
+                xhr.setRequestHeader(key, value);
+            }
+            xhr.send(body);
+        });
+        send.abort = () => {
+            console.log(`[Api] Publish aborted by user`);
+            xhr.abort();
+        }
+        return send;
+    }
+
     async auth(baseUrl, topic, user) {
     async auth(baseUrl, topic, user) {
         const url = topicUrlAuth(baseUrl, topic);
         const url = topicUrlAuth(baseUrl, topic);
         console.log(`[Api] Checking auth for ${url}`);
         console.log(`[Api] Checking auth for ${url}`);

+ 76 - 33
web/src/components/SendDialog.js

@@ -18,7 +18,7 @@ import IconButton from "@mui/material/IconButton";
 import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon';
 import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon';
 import {Close} from "@mui/icons-material";
 import {Close} from "@mui/icons-material";
 import MenuItem from "@mui/material/MenuItem";
 import MenuItem from "@mui/material/MenuItem";
-import {formatBytes, shortUrl, splitNoEmpty, splitTopicUrl, validTopicUrl} from "../app/utils";
+import {basicAuth, formatBytes, shortUrl, splitNoEmpty, splitTopicUrl, validTopicUrl} from "../app/utils";
 import Box from "@mui/material/Box";
 import Box from "@mui/material/Box";
 import Icon from "./Icon";
 import Icon from "./Icon";
 import DialogFooter from "./DialogFooter";
 import DialogFooter from "./DialogFooter";
@@ -26,6 +26,7 @@ import api from "../app/Api";
 import Divider from "@mui/material/Divider";
 import Divider from "@mui/material/Divider";
 import EditIcon from '@mui/icons-material/Edit';
 import EditIcon from '@mui/icons-material/Edit';
 import CheckIcon from '@mui/icons-material/Check';
 import CheckIcon from '@mui/icons-material/Check';
+import userManager from "../app/UserManager";
 
 
 const SendDialog = (props) => {
 const SendDialog = (props) => {
     const [topicUrl, setTopicUrl] = useState(props.topicUrl);
     const [topicUrl, setTopicUrl] = useState(props.topicUrl);
@@ -50,7 +51,9 @@ const SendDialog = (props) => {
     const showAttachFile = !!attachFile && !showAttachUrl;
     const showAttachFile = !!attachFile && !showAttachUrl;
     const attachFileInput = useRef();
     const attachFileInput = useRef();
 
 
-    const [errorText, setErrorText] = useState("");
+    const [sendRequest, setSendRequest] = useState(null);
+    const [statusText, setStatusText] = useState("");
+    const disabled = !!sendRequest;
 
 
     const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
     const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
     const sendButtonEnabled = (() => {
     const sendButtonEnabled = (() => {
@@ -61,38 +64,59 @@ const SendDialog = (props) => {
     })();
     })();
     const handleSubmit = async () => {
     const handleSubmit = async () => {
         const { baseUrl, topic } = splitTopicUrl(topicUrl);
         const { baseUrl, topic } = splitTopicUrl(topicUrl);
-        const options = {};
+        const headers = {};
         if (title.trim()) {
         if (title.trim()) {
-            options["title"] = title.trim();
+            headers["X-Title"] = title.trim();
         }
         }
         if (tags.trim()) {
         if (tags.trim()) {
-            options["tags"] = splitNoEmpty(tags, ",");
+            headers["X-Tags"] = tags.trim();
         }
         }
         if (priority && priority !== 3) {
         if (priority && priority !== 3) {
-            options["priority"] = priority;
+            headers["X-Priority"] = priority.toString();
         }
         }
         if (clickUrl.trim()) {
         if (clickUrl.trim()) {
-            options["click"] = clickUrl.trim();
+            headers["X-Click"] = clickUrl.trim();
         }
         }
         if (attachUrl.trim()) {
         if (attachUrl.trim()) {
-            options["attach"] = attachUrl.trim();
+            headers["X-Attach"] = attachUrl.trim();
         }
         }
         if (filename.trim()) {
         if (filename.trim()) {
-            options["filename"] = filename.trim();
+            headers["X-Filename"] = filename.trim();
         }
         }
         if (email.trim()) {
         if (email.trim()) {
-            options["email"] = email.trim();
+            headers["X-Email"] = email.trim();
         }
         }
         if (delay.trim()) {
         if (delay.trim()) {
-            options["delay"] = delay.trim();
+            headers["X-Delay"] = delay.trim();
         }
         }
+        if (attachFile && message.trim()) {
+            headers["X-Message"] = message.replaceAll("\n", "\\n").trim();
+        }
+        const body = (attachFile) ? attachFile : message;
         try {
         try {
-            const response = await api.publish(baseUrl, topic, message, options);
-            console.log(response);
-            props.onClose();
+            const user = await userManager.get(baseUrl);
+            if (user) {
+                headers["Authorization"] = basicAuth(user.username, user.password);
+            }
+            const progressFn = (ev) => {
+                console.log(ev);
+                if (ev.loaded > 0 && ev.total > 0) {
+                    const percent = Math.round(ev.loaded * 100.0 / ev.total);
+                    setStatusText(`Uploading ${formatBytes(ev.loaded)}/${formatBytes(ev.total)} (${percent}%) ...`);
+                } else {
+                    setStatusText(`Uploading ...`);
+                }
+            };
+            const request = api.publishXHR(baseUrl, topic, body, headers, progressFn);
+            setSendRequest(request);
+            await request;
+            setStatusText("Message published");
+            //props.onClose();
         } catch (e) {
         } catch (e) {
-            setErrorText(e);
+            console.log("error", e);
+            setStatusText("An error occurred");
         }
         }
+        setSendRequest(null);
     };
     };
     const handleAttachFileClick = () => {
     const handleAttachFileClick = () => {
         attachFileInput.current.click();
         attachFileInput.current.click();
@@ -109,7 +133,7 @@ const SendDialog = (props) => {
             <DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle>
             <DialogTitle>Publish to {shortUrl(topicUrl)}</DialogTitle>
             <DialogContent>
             <DialogContent>
                 {showTopicUrl &&
                 {showTopicUrl &&
-                    <ClosableRow onClose={() => {
+                    <ClosableRow disabled={disabled} onClose={() => {
                         setTopicUrl(props.topicUrl);
                         setTopicUrl(props.topicUrl);
                         setShowTopicUrl(false);
                         setShowTopicUrl(false);
                     }}>
                     }}>
@@ -118,6 +142,7 @@ const SendDialog = (props) => {
                             label="Topic URL"
                             label="Topic URL"
                             value={topicUrl}
                             value={topicUrl}
                             onChange={ev => setTopicUrl(ev.target.value)}
                             onChange={ev => setTopicUrl(ev.target.value)}
+                            disabled={disabled}
                             type="text"
                             type="text"
                             variant="standard"
                             variant="standard"
                             fullWidth
                             fullWidth
@@ -130,6 +155,7 @@ const SendDialog = (props) => {
                     label="Title"
                     label="Title"
                     value={title}
                     value={title}
                     onChange={ev => setTitle(ev.target.value)}
                     onChange={ev => setTitle(ev.target.value)}
+                    disabled={disabled}
                     type="text"
                     type="text"
                     fullWidth
                     fullWidth
                     variant="standard"
                     variant="standard"
@@ -141,6 +167,7 @@ const SendDialog = (props) => {
                     placeholder="Type the main message body here."
                     placeholder="Type the main message body here."
                     value={message}
                     value={message}
                     onChange={ev => setMessage(ev.target.value)}
                     onChange={ev => setMessage(ev.target.value)}
+                    disabled={disabled}
                     type="text"
                     type="text"
                     variant="standard"
                     variant="standard"
                     rows={5}
                     rows={5}
@@ -149,13 +176,14 @@ const SendDialog = (props) => {
                     multiline
                     multiline
                 />
                 />
                 <div style={{display: 'flex'}}>
                 <div style={{display: 'flex'}}>
-                    <DialogIconButton onClick={() => null}><InsertEmoticonIcon/></DialogIconButton>
+                    <DialogIconButton disabled={disabled} onClick={() => null}><InsertEmoticonIcon/></DialogIconButton>
                     <TextField
                     <TextField
                         margin="dense"
                         margin="dense"
                         label="Tags"
                         label="Tags"
                         placeholder="Comma-separated list of tags, e.g. warning, srv1-backup"
                         placeholder="Comma-separated list of tags, e.g. warning, srv1-backup"
                         value={tags}
                         value={tags}
                         onChange={ev => setTags(ev.target.value)}
                         onChange={ev => setTags(ev.target.value)}
+                        disabled={disabled}
                         type="text"
                         type="text"
                         variant="standard"
                         variant="standard"
                         sx={{flexGrow: 1, marginRight: 1}}
                         sx={{flexGrow: 1, marginRight: 1}}
@@ -171,6 +199,7 @@ const SendDialog = (props) => {
                             margin="dense"
                             margin="dense"
                             value={priority}
                             value={priority}
                             onChange={(ev) => setPriority(ev.target.value)}
                             onChange={(ev) => setPriority(ev.target.value)}
+                            disabled={disabled}
                         >
                         >
                             {[5,4,3,2,1].map(priority =>
                             {[5,4,3,2,1].map(priority =>
                                 <MenuItem value={priority}>
                                 <MenuItem value={priority}>
@@ -184,7 +213,7 @@ const SendDialog = (props) => {
                     </FormControl>
                     </FormControl>
                 </div>
                 </div>
                 {showClickUrl &&
                 {showClickUrl &&
-                    <ClosableRow onClose={() => {
+                    <ClosableRow disabled={disabled} onClose={() => {
                         setClickUrl("");
                         setClickUrl("");
                         setShowClickUrl(false);
                         setShowClickUrl(false);
                     }}>
                     }}>
@@ -194,6 +223,7 @@ const SendDialog = (props) => {
                             placeholder="URL that is opened when notification is clicked"
                             placeholder="URL that is opened when notification is clicked"
                             value={clickUrl}
                             value={clickUrl}
                             onChange={ev => setClickUrl(ev.target.value)}
                             onChange={ev => setClickUrl(ev.target.value)}
+                            disabled={disabled}
                             type="url"
                             type="url"
                             fullWidth
                             fullWidth
                             variant="standard"
                             variant="standard"
@@ -201,7 +231,7 @@ const SendDialog = (props) => {
                     </ClosableRow>
                     </ClosableRow>
                 }
                 }
                 {showEmail &&
                 {showEmail &&
-                    <ClosableRow onClose={() => {
+                    <ClosableRow disabled={disabled} onClose={() => {
                         setEmail("");
                         setEmail("");
                         setShowEmail(false);
                         setShowEmail(false);
                     }}>
                     }}>
@@ -211,6 +241,7 @@ const SendDialog = (props) => {
                             placeholder="Address to forward the message to, e.g. phil@example.com"
                             placeholder="Address to forward the message to, e.g. phil@example.com"
                             value={email}
                             value={email}
                             onChange={ev => setEmail(ev.target.value)}
                             onChange={ev => setEmail(ev.target.value)}
+                            disabled={disabled}
                             type="email"
                             type="email"
                             variant="standard"
                             variant="standard"
                             fullWidth
                             fullWidth
@@ -218,7 +249,7 @@ const SendDialog = (props) => {
                     </ClosableRow>
                     </ClosableRow>
                 }
                 }
                 {showAttachUrl &&
                 {showAttachUrl &&
-                    <ClosableRow onClose={() => {
+                    <ClosableRow disabled={disabled} onClose={() => {
                         setAttachUrl("");
                         setAttachUrl("");
                         setFilename("");
                         setFilename("");
                         setFilenameEdited(false);
                         setFilenameEdited(false);
@@ -244,6 +275,7 @@ const SendDialog = (props) => {
                                     }
                                     }
                                 }
                                 }
                             }}
                             }}
+                            disabled={disabled}
                             type="url"
                             type="url"
                             variant="standard"
                             variant="standard"
                             sx={{flexGrow: 5, marginRight: 1}}
                             sx={{flexGrow: 5, marginRight: 1}}
@@ -257,6 +289,7 @@ const SendDialog = (props) => {
                                 setFilename(ev.target.value);
                                 setFilename(ev.target.value);
                                 setFilenameEdited(true);
                                 setFilenameEdited(true);
                             }}
                             }}
+                            disabled={disabled}
                             type="text"
                             type="text"
                             variant="standard"
                             variant="standard"
                             sx={{flexGrow: 1}}
                             sx={{flexGrow: 1}}
@@ -272,6 +305,7 @@ const SendDialog = (props) => {
                 {showAttachFile && <AttachmentBox
                 {showAttachFile && <AttachmentBox
                     file={attachFile}
                     file={attachFile}
                     filename={filename}
                     filename={filename}
+                    disabled={disabled}
                     onChangeFilename={(f) => setFilename(f)}
                     onChangeFilename={(f) => setFilename(f)}
                     onClose={() => {
                     onClose={() => {
                         setAttachFile(null);
                         setAttachFile(null);
@@ -279,7 +313,7 @@ const SendDialog = (props) => {
                     }}
                     }}
                 />}
                 />}
                 {showDelay &&
                 {showDelay &&
-                    <ClosableRow onClose={() => {
+                    <ClosableRow disabled={disabled} onClose={() => {
                         setDelay("");
                         setDelay("");
                         setShowDelay(false);
                         setShowDelay(false);
                     }}>
                     }}>
@@ -289,6 +323,7 @@ const SendDialog = (props) => {
                             placeholder="Unix timestamp, duration or English natural language"
                             placeholder="Unix timestamp, duration or English natural language"
                             value={delay}
                             value={delay}
                             onChange={ev => setDelay(ev.target.value)}
                             onChange={ev => setDelay(ev.target.value)}
+                            disabled={disabled}
                             type="text"
                             type="text"
                             variant="standard"
                             variant="standard"
                             fullWidth
                             fullWidth
@@ -299,21 +334,26 @@ const SendDialog = (props) => {
                     Other features:
                     Other features:
                 </Typography>
                 </Typography>
                 <div>
                 <div>
-                    {!showClickUrl && <Chip clickable label="Click URL" onClick={() => setShowClickUrl(true)} sx={{marginRight: 1}}/>}
-                    {!showEmail && <Chip clickable label="Forward to email" onClick={() => setShowEmail(true)} sx={{marginRight: 1}}/>}
-                    {!showAttachUrl && !showAttachFile && <Chip clickable label="Attach file by URL" onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1}}/>}
-                    {!showAttachFile && !showAttachUrl && <Chip clickable label="Attach local file" onClick={() => handleAttachFileClick()} sx={{marginRight: 1}}/>}
-                    {!showDelay && <Chip clickable label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1}}/>}
-                    {!showTopicUrl && <Chip clickable label="Change topic" onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1}}/>}
+                    {!showClickUrl && <Chip clickable disabled={disabled} label="Click URL" onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                    {!showEmail && <Chip clickable disabled={disabled} label="Forward to email" onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                    {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label="Attach file by URL" onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                    {!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label="Attach local file" onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
+                    {!showDelay && <Chip clickable disabled={disabled} label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                    {!showTopicUrl && <Chip clickable disabled={disabled} label="Change topic" onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
                 </div>
                 </div>
-                <Typography variant="body1" sx={{marginTop: 2, marginBottom: 1}}>
+                <Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
                     For examples and a detailed description of all send features, please
                     For examples and a detailed description of all send features, please
                     refer to the <Link href="/docs">documentation</Link>.
                     refer to the <Link href="/docs">documentation</Link>.
                 </Typography>
                 </Typography>
             </DialogContent>
             </DialogContent>
-            <DialogFooter status={errorText}>
-                <Button onClick={props.onClose}>Cancel</Button>
-                <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
+            <DialogFooter status={statusText}>
+                {sendRequest && <Button onClick={() => sendRequest.abort()}>Cancel sending</Button>}
+                {!sendRequest &&
+                    <>
+                        <Button onClick={props.onClose}>Cancel</Button>
+                        <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
+                    </>
+                }
             </DialogFooter>
             </DialogFooter>
         </Dialog>
         </Dialog>
     );
     );
@@ -331,7 +371,7 @@ const ClosableRow = (props) => {
     return (
     return (
         <Row>
         <Row>
             {props.children}
             {props.children}
-            <DialogIconButton onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
+            <DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
         </Row>
         </Row>
     );
     );
 };
 };
@@ -345,6 +385,7 @@ const DialogIconButton = (props) => {
             edge="start"
             edge="start"
             sx={{height: "45px", marginTop: "17px", ...sx}}
             sx={{height: "45px", marginTop: "17px", ...sx}}
             onClick={props.onClick}
             onClick={props.onClick}
+            disabled={props.disabled}
         >
         >
             {props.children}
             {props.children}
         </IconButton>
         </IconButton>
@@ -371,11 +412,12 @@ const AttachmentBox = (props) => {
                         variant="body2"
                         variant="body2"
                         value={props.filename}
                         value={props.filename}
                         onChange={(ev) => props.onChangeFilename(ev.target.value)}
                         onChange={(ev) => props.onChangeFilename(ev.target.value)}
+                        disabled={props.disabled}
                     />
                     />
                     <br/>
                     <br/>
                     {formatBytes(file.size)}
                     {formatBytes(file.size)}
                 </Typography>
                 </Typography>
-                <DialogIconButton onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
+                <DialogIconButton disabled={props.disabled} onClick={props.onClose} sx={{marginLeft: "6px"}}><Close/></DialogIconButton>
             </Box>
             </Box>
         </>
         </>
     );
     );
@@ -414,6 +456,7 @@ const ExpandingTextField = (props) => {
                 sx={{ width: `${textWidth}px`, borderBottom: "none" }}
                 sx={{ width: `${textWidth}px`, borderBottom: "none" }}
                 InputProps={{ style: { fontSize: theme.typography[props.variant].fontSize } }}
                 InputProps={{ style: { fontSize: theme.typography[props.variant].fontSize } }}
                 inputProps={{ style: { paddingBottom: 0, paddingTop: 0 } }}
                 inputProps={{ style: { paddingBottom: 0, paddingTop: 0 } }}
+                disabled={props.disabled}
             />
             />
         </>
         </>
     )
     )