From 78485768fec8d5212075be26a5ca22db2d8e288f Mon Sep 17 00:00:00 2001 From: postrowinski Date: Wed, 19 Apr 2023 14:53:34 +0200 Subject: [PATCH 01/54] add basic routing for upload config and blob and add basic configUpload view --- mwdb/web/src/App.jsx | 25 ++- mwdb/web/src/commons/api/index.jsx | 5 + mwdb/web/src/commons/hooks/index.js | 1 + .../src/commons/hooks/useJsonParseError.js | 18 ++ mwdb/web/src/commons/ui/NavDropdown.jsx | 12 +- mwdb/web/src/components/Navigation.jsx | 55 ++++- .../src/components/Upload/UploadConfig.jsx | 204 ++++++++++++++++++ .../{Upload.jsx => Upload/UploadFile.jsx} | 10 +- 8 files changed, 305 insertions(+), 25 deletions(-) create mode 100644 mwdb/web/src/commons/hooks/index.js create mode 100644 mwdb/web/src/commons/hooks/useJsonParseError.js create mode 100644 mwdb/web/src/components/Upload/UploadConfig.jsx rename mwdb/web/src/components/{Upload.jsx => Upload/UploadFile.jsx} (98%) diff --git a/mwdb/web/src/App.jsx b/mwdb/web/src/App.jsx index 9598c578a..11c53ae3e 100644 --- a/mwdb/web/src/App.jsx +++ b/mwdb/web/src/App.jsx @@ -17,7 +17,7 @@ import ShowSample from "./components/ShowSample"; import ShowConfig from "./components/ShowConfig"; import ShowTextBlob from "./components/ShowTextBlob"; import DiffTextBlob from "./components/DiffTextBlob"; -import Upload from "./components/Upload"; +import UploadFile from "./components/Upload/UploadFile"; import UserLogin from "./components/UserLogin"; import UserRegister from "./components/UserRegister"; import UserSetPassword from "./components/UserSetPassword"; @@ -71,6 +71,7 @@ import { Capability } from "./commons/auth"; import { ConfigContext } from "./commons/config"; import { fromPlugins, Extendable } from "./commons/plugins"; import { ErrorBoundary, RequiresAuth, RequiresCapability } from "./commons/ui"; +import UploadConfig from "./components/Upload/UploadConfig"; function NavigateFor404() { /** @@ -109,10 +110,28 @@ function AppRoutes() { } /> } /> - + + + } + /> + + + + } + /> + + } /> diff --git a/mwdb/web/src/commons/api/index.jsx b/mwdb/web/src/commons/api/index.jsx index f5d0d6487..d50024a1e 100644 --- a/mwdb/web/src/commons/api/index.jsx +++ b/mwdb/web/src/commons/api/index.jsx @@ -444,6 +444,10 @@ function uploadFile(file, parent, upload_as, attributes, fileUploadTimeout) { return axios.post(`/file`, formData, { timeout: fileUploadTimeout }); } +function uploadConfig(body) { + return axios.post("/config", body); +} + function getRemoteNames() { return axios.get("/remote"); } @@ -627,6 +631,7 @@ export const api = { requestFileDownloadLink, requestZipFileDownloadLink, uploadFile, + uploadConfig, getRemoteNames, pushObjectRemote, pullObjectRemote, diff --git a/mwdb/web/src/commons/hooks/index.js b/mwdb/web/src/commons/hooks/index.js new file mode 100644 index 000000000..2e1e8b4a6 --- /dev/null +++ b/mwdb/web/src/commons/hooks/index.js @@ -0,0 +1 @@ +export { useJsonParseError } from "./useJsonParseError"; diff --git a/mwdb/web/src/commons/hooks/useJsonParseError.js b/mwdb/web/src/commons/hooks/useJsonParseError.js new file mode 100644 index 000000000..cd83a407d --- /dev/null +++ b/mwdb/web/src/commons/hooks/useJsonParseError.js @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; + +export function useJsonParseError(value) { + const [errorMessage, setErrorMessage] = useState(""); + + useEffect(() => { + try { + JSON.parse(value); + setErrorMessage(""); + } catch (e) { + setErrorMessage(e.toString()); + } + }, [value]); + + return { + errorMessage, + }; +} diff --git a/mwdb/web/src/commons/ui/NavDropdown.jsx b/mwdb/web/src/commons/ui/NavDropdown.jsx index e06eb462e..5c4258c50 100644 --- a/mwdb/web/src/commons/ui/NavDropdown.jsx +++ b/mwdb/web/src/commons/ui/NavDropdown.jsx @@ -4,36 +4,32 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export default function NavDropdown(props) { if (!props.elements.length) return ); } diff --git a/mwdb/web/src/components/Navigation.jsx b/mwdb/web/src/components/Navigation.jsx index e711f0c0d..e93a9e932 100644 --- a/mwdb/web/src/components/Navigation.jsx +++ b/mwdb/web/src/components/Navigation.jsx @@ -63,21 +63,56 @@ function RemoteDropdown() { ); } -function UploadButton() { +function UploadDropdown() { + const uploadElement = [ + { + capability: Capability.addingFiles, + type: "File", + link: "/file_upload", + }, + { + capability: Capability.addingBlobs, + type: "Blob", + link: "/blob_upload", + }, + { + capability: Capability.addingConfigs, + type: "Config", + link: "/config_upload", + }, + ]; + return ( + { + return ( + + ); + })} + /> + ); +} + +function UploadButton({ capability, type, link }) { const auth = useContext(AuthContext); - const buttonLink = auth.hasCapability(Capability.addingFiles) ? ( - - - Upload + const buttonLink = auth.hasCapability(capability) ? ( + + {type} ) : ( -
+
- - Upload + {type}
); @@ -123,7 +158,7 @@ export default function Navigation() {
  • - +
  • ) : ( diff --git a/mwdb/web/src/components/Upload/UploadConfig.jsx b/mwdb/web/src/components/Upload/UploadConfig.jsx new file mode 100644 index 000000000..92bfa1f2a --- /dev/null +++ b/mwdb/web/src/components/Upload/UploadConfig.jsx @@ -0,0 +1,204 @@ +import React, { useContext, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { toast } from "react-toastify"; +import { isEmpty } from "lodash"; +import AceEditor from "react-ace"; + +import AttributesAddModal from "../AttributesAddModal"; + +import { api } from "@mwdb-web/commons/api"; +import { AuthContext, Capability } from "@mwdb-web/commons/auth"; +import { DataTable, View, getErrorMessage } from "@mwdb-web/commons/ui"; +import { Extendable } from "@mwdb-web/commons/plugins"; +import { useJsonParseError } from "@mwdb-web/commons/hooks"; + +export default function UploadConfig() { + const auth = useContext(AuthContext); + const navigate = useNavigate(); + const searchParams = useSearchParams()[0]; + const [cfg, setCfg] = useState("{}"); + + const [parent, setParent] = useState(""); + const [family, setFamily] = useState(""); + const [attributes, setAttributes] = useState([]); + const [attributeModalOpen, setAttributeModalOpen] = useState(false); + const { errorMessage: cfgErrorMessage } = useJsonParseError(cfg); + + const handleParentChange = (ev) => { + ev.preventDefault(); + setParent(ev.target.value); + }; + + const handleParentClear = () => { + if (searchParams.get("parent")) navigate("/upload"); + setParent(""); + }; + + const handleSubmit = async () => { + try { + const body = { + cfg: JSON.parse(cfg), + family, + parent: !isEmpty(parent) ? parent : undefined, + attributes, + }; + console.log(body); + let response = await api.uploadConfig(body); + navigate("/config/" + response.data.id, { + replace: true, + }); + toast("File uploaded successfully.", { + type: "success", + }); + } catch (error) { + toast(getErrorMessage(error), { + type: "error", + }); + } + }; + + const onAttributeAdd = (key, value) => { + for (let attr of attributes) + if (attr.key === key && attr.value === value) { + // that key, value was added yet + setAttributeModalOpen(false); + return; + } + setAttributes([...attributes, { key, value }]); + setAttributeModalOpen(false); + }; + + const onAttributeRemove = (idx) => { + setAttributes([ + ...attributes.slice(0, idx), + ...attributes.slice(idx + 1), + ]); + }; + + return ( + + +
    +
    + setCfg(input)} + value={cfg} + width="500px" + height="150px" + setOptions={{ + useWorker: false, + }} + /> +
    +
    + +
    + setFamily(e.target.value)} + /> +
    + setFamily("")} + /> +
    +
    + {auth.hasCapability(Capability.addingParents) && ( +
    +
    + +
    + +
    + +
    +
    + )} + {attributes.length > 0 && ( +
    +
    Attributes
    + + {attributes.map((attr, idx) => ( + + {attr.key} + + {typeof attr.value === + "string" ? ( + attr.value + ) : ( +
    +                                                        {"(object)"}{" "}
    +                                                        {JSON.stringify(
    +                                                            attr.value,
    +                                                            null,
    +                                                            4
    +                                                        )}
    +                                                    
    + )} + + + + onAttributeRemove(idx) + } + /> + + + ))} +
    +
    + )} +
    {cfgErrorMessage}
    + + setAttributeModalOpen(true)} + /> +
    +
    +
    + setAttributeModalOpen(false)} + onAdd={onAttributeAdd} + /> +
    + ); +} diff --git a/mwdb/web/src/components/Upload.jsx b/mwdb/web/src/components/Upload/UploadFile.jsx similarity index 98% rename from mwdb/web/src/components/Upload.jsx rename to mwdb/web/src/components/Upload/UploadFile.jsx index 7e2909c4f..2417af59a 100644 --- a/mwdb/web/src/components/Upload.jsx +++ b/mwdb/web/src/components/Upload/UploadFile.jsx @@ -5,7 +5,7 @@ import { toast } from "react-toastify"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faUpload } from "@fortawesome/free-solid-svg-icons"; -import AttributesAddModal from "./AttributesAddModal"; +import AttributesAddModal from "../AttributesAddModal"; import { api } from "@mwdb-web/commons/api"; import { AuthContext, Capability } from "@mwdb-web/commons/auth"; @@ -61,7 +61,7 @@ function UploadDropzone(props) { ); } -export default function Upload() { +export default function UploadFile() { const auth = useContext(AuthContext); const config = useContext(ConfigContext); const fileUploadTimeout = config.config["file_upload_timeout"]; @@ -109,6 +109,8 @@ export default function Upload() { return `The sample will be accessible only for you and chosen group.`; }; + console.log(parent); + const handleSubmit = async () => { try { let response = await api.uploadFile( @@ -169,8 +171,8 @@ export default function Upload() { } return ( - - + +
    Date: Thu, 20 Apr 2023 09:56:19 +0200 Subject: [PATCH 02/54] add full working with api config form TODO: try to refactor to react-hook-form --- mwdb/web/src/commons/ui/FormError.jsx | 15 +++ mwdb/web/src/commons/ui/index.jsx | 1 + .../src/components/Upload/UploadConfig.jsx | 115 +++++++++++++----- mwdb/web/src/components/Upload/UploadFile.jsx | 37 +++--- 4 files changed, 121 insertions(+), 47 deletions(-) create mode 100644 mwdb/web/src/commons/ui/FormError.jsx diff --git a/mwdb/web/src/commons/ui/FormError.jsx b/mwdb/web/src/commons/ui/FormError.jsx new file mode 100644 index 000000000..ad9043569 --- /dev/null +++ b/mwdb/web/src/commons/ui/FormError.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { isNil } from "lodash"; + +export default function FormError(props) { + const { errorField } = props; + if (isNil(errorField)) { + return <>; + } + + return ( +
    + {errorField.message} +
    + ); +} diff --git a/mwdb/web/src/commons/ui/index.jsx b/mwdb/web/src/commons/ui/index.jsx index f457e3dc9..7b6f5bb84 100644 --- a/mwdb/web/src/commons/ui/index.jsx +++ b/mwdb/web/src/commons/ui/index.jsx @@ -20,6 +20,7 @@ export { default as SortedList } from "./SortedList"; export { default as View, useViewAlert } from "./View"; export { default as ActionCopyToClipboard } from "./ActionCopyToClipboard"; export { RequiresAuth, RequiresCapability } from "./RequiresAuth"; +export { default as FormError } from "./FormError"; export { Tag, TagList, getStyleForTag } from "./Tag"; export { diff --git a/mwdb/web/src/components/Upload/UploadConfig.jsx b/mwdb/web/src/components/Upload/UploadConfig.jsx index 92bfa1f2a..69d71247f 100644 --- a/mwdb/web/src/components/Upload/UploadConfig.jsx +++ b/mwdb/web/src/components/Upload/UploadConfig.jsx @@ -8,7 +8,12 @@ import AttributesAddModal from "../AttributesAddModal"; import { api } from "@mwdb-web/commons/api"; import { AuthContext, Capability } from "@mwdb-web/commons/auth"; -import { DataTable, View, getErrorMessage } from "@mwdb-web/commons/ui"; +import { + DataTable, + View, + getErrorMessage, + FormError, +} from "@mwdb-web/commons/ui"; import { Extendable } from "@mwdb-web/commons/plugins"; import { useJsonParseError } from "@mwdb-web/commons/hooks"; @@ -20,6 +25,7 @@ export default function UploadConfig() { const [parent, setParent] = useState(""); const [family, setFamily] = useState(""); + const [configType, setConfigType] = useState("static"); const [attributes, setAttributes] = useState([]); const [attributeModalOpen, setAttributeModalOpen] = useState(false); const { errorMessage: cfgErrorMessage } = useJsonParseError(cfg); @@ -30,7 +36,7 @@ export default function UploadConfig() { }; const handleParentClear = () => { - if (searchParams.get("parent")) navigate("/upload"); + if (searchParams.get("parent")) navigate("/config_upload"); setParent(""); }; @@ -40,14 +46,14 @@ export default function UploadConfig() { cfg: JSON.parse(cfg), family, parent: !isEmpty(parent) ? parent : undefined, + config_type: configType, attributes, }; - console.log(body); let response = await api.uploadConfig(body); navigate("/config/" + response.data.id, { replace: true, }); - toast("File uploaded successfully.", { + toast("Config uploaded successfully.", { type: "success", }); } catch (error) { @@ -80,25 +86,60 @@ export default function UploadConfig() {
    - setCfg(input)} - value={cfg} - width="500px" - height="150px" - setOptions={{ - useWorker: false, - }} - /> +
    +
    + + setCfg(input)} + value={cfg} + height="260px" + setOptions={{ + useWorker: false, + }} + /> +
    +
    + + +
    +
    + +
    + +
    -
    -
    )} -
    {cfgErrorMessage}
    - - setAttributeModalOpen(true)} - /> +
    + setAttributeModalOpen(true)} + /> + +
    diff --git a/mwdb/web/src/components/Upload/UploadFile.jsx b/mwdb/web/src/components/Upload/UploadFile.jsx index 2417af59a..bd6a3bd8d 100644 --- a/mwdb/web/src/components/Upload/UploadFile.jsx +++ b/mwdb/web/src/components/Upload/UploadFile.jsx @@ -82,7 +82,7 @@ export default function UploadFile() { }; const handleParentClear = () => { - if (searchParams.get("parent")) navigate("/upload"); + if (searchParams.get("parent")) navigate("/file_upload"); setParent(""); }; @@ -109,8 +109,6 @@ export default function UploadFile() { return `The sample will be accessible only for you and chosen group.`; }; - console.log(parent); - const handleSubmit = async () => { try { let response = await api.uploadFile( @@ -303,19 +301,26 @@ export default function UploadFile() { ) : ( [] )} - - setAttributeModalOpen(true)} - /> +
    + setAttributeModalOpen(true)} + /> + +
    From bd9303e942f5cea2aca5b974afbb2b7ef23c2a2d Mon Sep 17 00:00:00 2001 From: postrowinski Date: Thu, 20 Apr 2023 10:39:21 +0200 Subject: [PATCH 03/54] change layout for config and file upload --- .../src/components/Upload/UploadConfig.jsx | 79 ++++++++--------- mwdb/web/src/components/Upload/UploadFile.jsx | 85 ++++++++----------- 2 files changed, 73 insertions(+), 91 deletions(-) diff --git a/mwdb/web/src/components/Upload/UploadConfig.jsx b/mwdb/web/src/components/Upload/UploadConfig.jsx index 69d71247f..23b53f566 100644 --- a/mwdb/web/src/components/Upload/UploadConfig.jsx +++ b/mwdb/web/src/components/Upload/UploadConfig.jsx @@ -186,55 +186,48 @@ export default function UploadConfig() { )} - {attributes.length > 0 && ( -
    -
    Attributes
    - - {attributes.map((attr, idx) => ( - - {attr.key} - - {typeof attr.value === - "string" ? ( - attr.value - ) : ( -
    -                                                        {"(object)"}{" "}
    -                                                        {JSON.stringify(
    -                                                            attr.value,
    -                                                            null,
    -                                                            4
    -                                                        )}
    -                                                    
    - )} - - - - onAttributeRemove(idx) - } - /> - - - ))} -
    -
    - )} -
    +
    +
    Attributes
    setAttributeModalOpen(true)} /> +
    + + + {attributes.map((attr, idx) => ( + + {attr.key} + + {typeof attr.value === "string" ? ( + attr.value + ) : ( +
    +                                                {"(object)"}{" "}
    +                                                {JSON.stringify(
    +                                                    attr.value,
    +                                                    null,
    +                                                    4
    +                                                )}
    +                                            
    + )} + + + + onAttributeRemove(idx) + } + /> + + + ))} +
    +
    - {shareWith === "single" ? ( + {shareWith === "single" && (
    - ) : ( - [] - )} - {attributes.length > 0 ? ( -
    -
    Attributes
    - - {attributes.map((attr, idx) => ( - - {attr.key} - - {typeof attr.value === - "string" ? ( - attr.value - ) : ( -
    -                                                        {"(object)"}{" "}
    -                                                        {JSON.stringify(
    -                                                            attr.value,
    -                                                            null,
    -                                                            4
    -                                                        )}
    -                                                    
    - )} - - - - onAttributeRemove(idx) - } - /> - - - ))} -
    -
    - ) : ( - [] )} -
    +
    +
    Attributes
    setAttributeModalOpen(true)} /> +
    + + {attributes.map((attr, idx) => ( + + {attr.key} + + {typeof attr.value === "string" ? ( + attr.value + ) : ( +
    +                                                {"(object)"}{" "}
    +                                                {JSON.stringify(
    +                                                    attr.value,
    +                                                    null,
    +                                                    4
    +                                                )}
    +                                            
    + )} + + + + onAttributeRemove(idx) + } + /> + + + ))} +
    + +
    Date: Fri, 21 Apr 2023 10:53:23 +0200 Subject: [PATCH 04/54] change native form states to react-hook-forms in UploadConfig --- mwdb/web/src/commons/hooks/index.js | 1 - .../src/commons/hooks/useJsonParseError.js | 18 -- .../src/components/Upload/UploadConfig.jsx | 164 +++++++++++------- 3 files changed, 100 insertions(+), 83 deletions(-) delete mode 100644 mwdb/web/src/commons/hooks/index.js delete mode 100644 mwdb/web/src/commons/hooks/useJsonParseError.js diff --git a/mwdb/web/src/commons/hooks/index.js b/mwdb/web/src/commons/hooks/index.js deleted file mode 100644 index 2e1e8b4a6..000000000 --- a/mwdb/web/src/commons/hooks/index.js +++ /dev/null @@ -1 +0,0 @@ -export { useJsonParseError } from "./useJsonParseError"; diff --git a/mwdb/web/src/commons/hooks/useJsonParseError.js b/mwdb/web/src/commons/hooks/useJsonParseError.js deleted file mode 100644 index cd83a407d..000000000 --- a/mwdb/web/src/commons/hooks/useJsonParseError.js +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect, useState } from "react"; - -export function useJsonParseError(value) { - const [errorMessage, setErrorMessage] = useState(""); - - useEffect(() => { - try { - JSON.parse(value); - setErrorMessage(""); - } catch (e) { - setErrorMessage(e.toString()); - } - }, [value]); - - return { - errorMessage, - }; -} diff --git a/mwdb/web/src/components/Upload/UploadConfig.jsx b/mwdb/web/src/components/Upload/UploadConfig.jsx index 23b53f566..0de2dc6e1 100644 --- a/mwdb/web/src/components/Upload/UploadConfig.jsx +++ b/mwdb/web/src/components/Upload/UploadConfig.jsx @@ -1,8 +1,11 @@ import React, { useContext, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; +import { useForm, useFieldArray } from "react-hook-form"; import { toast } from "react-toastify"; import { isEmpty } from "lodash"; import AceEditor from "react-ace"; +import * as Yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; import AttributesAddModal from "../AttributesAddModal"; @@ -15,39 +18,86 @@ import { FormError, } from "@mwdb-web/commons/ui"; import { Extendable } from "@mwdb-web/commons/plugins"; -import { useJsonParseError } from "@mwdb-web/commons/hooks"; + +const formFields = { + cfg: "cfg", + family: "family", + parent: "parent", + config_type: "config_type", + attributes: "attributes", +}; + +const validationSchema = Yup.object().shape({ + [formFields.cfg]: Yup.string().test({ + message: ({ value }) => { + if (!value) { + return ""; + } + try { + JSON.parse(value); + return ""; + } catch (err) { + return err.toString(); + } + }, + test: (value) => { + try { + JSON.parse(value); + return true; + } catch (err) { + return false; + } + }, + }), +}); export default function UploadConfig() { const auth = useContext(AuthContext); const navigate = useNavigate(); const searchParams = useSearchParams()[0]; - const [cfg, setCfg] = useState("{}"); - - const [parent, setParent] = useState(""); - const [family, setFamily] = useState(""); - const [configType, setConfigType] = useState("static"); - const [attributes, setAttributes] = useState([]); const [attributeModalOpen, setAttributeModalOpen] = useState(false); - const { errorMessage: cfgErrorMessage } = useJsonParseError(cfg); - const handleParentChange = (ev) => { - ev.preventDefault(); - setParent(ev.target.value); + const formOptions = { + resolver: yupResolver(validationSchema), + mode: "onSubmit", + reValidateMode: "onSubmit", + defaultValues: { + [formFields.cfg]: "{}", + [formFields.parent]: searchParams.get("parent") || "", + [formFields.attributes]: [], + }, }; + const { + register, + setValue, + handleSubmit, + formState: { errors }, + control, + } = useForm(formOptions); + + const { + fields: attributes, + append: appendAttribute, + remove: removeAttribute, + } = useFieldArray({ + control, + name: formFields.attributes, + }); + const handleParentClear = () => { if (searchParams.get("parent")) navigate("/config_upload"); - setParent(""); + setValue(formFields.parent, ""); }; - const handleSubmit = async () => { + const onSubmit = async (values) => { try { const body = { - cfg: JSON.parse(cfg), - family, - parent: !isEmpty(parent) ? parent : undefined, - config_type: configType, - attributes, + ...values, + [formFields.cfg]: JSON.parse(values[formFields.cfg]), + [formFields.parent]: !isEmpty(values[formFields.parent]) + ? values[formFields.parent] + : undefined, }; let response = await api.uploadConfig(body); navigate("/config/" + response.data.id, { @@ -70,60 +120,51 @@ export default function UploadConfig() { setAttributeModalOpen(false); return; } - setAttributes([...attributes, { key, value }]); + appendAttribute({ key, value }); setAttributeModalOpen(false); }; - const onAttributeRemove = (idx) => { - setAttributes([ - ...attributes.slice(0, idx), - ...attributes.slice(idx + 1), - ]); - }; - return ( -
    +
    -
    - +
    setFamily(e.target.value)} />
    setFamily("")} + onClick={() => + setValue(formFields.family, "") + } />
    @@ -160,20 +202,19 @@ export default function UploadConfig() {
    @@ -219,9 +260,7 @@ export default function UploadConfig() { value="Dismiss" className="btn btn-danger" type="button" - onClick={() => - onAttributeRemove(idx) - } + onClick={() => removeAttribute(idx)} /> @@ -231,10 +270,7 @@ export default function UploadConfig() {
    From 07272c74df4f8569c17ca7e347b7d456c5998c69 Mon Sep 17 00:00:00 2001 From: postrowinski Date: Fri, 21 Apr 2023 14:58:57 +0200 Subject: [PATCH 05/54] rewrite UploadFile to react-hook-forms and separate UploadDropzone and Attributes components --- mwdb/web/src/App.jsx | 10 +- ...{UploadConfig.jsx => UploadConfigView.jsx} | 123 ++----- mwdb/web/src/components/Upload/UploadFile.jsx | 323 ------------------ .../src/components/Upload/UploadFileView.jsx | 260 ++++++++++++++ .../Upload/components/Attributes.jsx | 62 ++++ .../Upload/components/UploadDropzone.jsx | 49 +++ 6 files changed, 406 insertions(+), 421 deletions(-) rename mwdb/web/src/components/Upload/{UploadConfig.jsx => UploadConfigView.jsx} (73%) delete mode 100644 mwdb/web/src/components/Upload/UploadFile.jsx create mode 100644 mwdb/web/src/components/Upload/UploadFileView.jsx create mode 100644 mwdb/web/src/components/Upload/components/Attributes.jsx create mode 100644 mwdb/web/src/components/Upload/components/UploadDropzone.jsx diff --git a/mwdb/web/src/App.jsx b/mwdb/web/src/App.jsx index 11c53ae3e..713976573 100644 --- a/mwdb/web/src/App.jsx +++ b/mwdb/web/src/App.jsx @@ -17,7 +17,8 @@ import ShowSample from "./components/ShowSample"; import ShowConfig from "./components/ShowConfig"; import ShowTextBlob from "./components/ShowTextBlob"; import DiffTextBlob from "./components/DiffTextBlob"; -import UploadFile from "./components/Upload/UploadFile"; +import UploadFileView from "./components/Upload/UploadFileView"; +import UploadConfigView from "./components/Upload/UploadConfigView"; import UserLogin from "./components/UserLogin"; import UserRegister from "./components/UserRegister"; import UserSetPassword from "./components/UserSetPassword"; @@ -71,7 +72,6 @@ import { Capability } from "./commons/auth"; import { ConfigContext } from "./commons/config"; import { fromPlugins, Extendable } from "./commons/plugins"; import { ErrorBoundary, RequiresAuth, RequiresCapability } from "./commons/ui"; -import UploadConfig from "./components/Upload/UploadConfig"; function NavigateFor404() { /** @@ -113,7 +113,7 @@ function AppRoutes() { path="file_upload" element={ - + } /> @@ -121,7 +121,7 @@ function AppRoutes() { path="blob_upload" element={ - + } /> @@ -131,7 +131,7 @@ function AppRoutes() { - + } /> diff --git a/mwdb/web/src/components/Upload/UploadConfig.jsx b/mwdb/web/src/components/Upload/UploadConfigView.jsx similarity index 73% rename from mwdb/web/src/components/Upload/UploadConfig.jsx rename to mwdb/web/src/components/Upload/UploadConfigView.jsx index 0de2dc6e1..7ab375435 100644 --- a/mwdb/web/src/components/Upload/UploadConfig.jsx +++ b/mwdb/web/src/components/Upload/UploadConfigView.jsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useContext } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useForm, useFieldArray } from "react-hook-form"; import { toast } from "react-toastify"; @@ -7,17 +7,11 @@ import AceEditor from "react-ace"; import * as Yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; -import AttributesAddModal from "../AttributesAddModal"; - import { api } from "@mwdb-web/commons/api"; import { AuthContext, Capability } from "@mwdb-web/commons/auth"; -import { - DataTable, - View, - getErrorMessage, - FormError, -} from "@mwdb-web/commons/ui"; +import { View, getErrorMessage, FormError } from "@mwdb-web/commons/ui"; import { Extendable } from "@mwdb-web/commons/plugins"; +import { Attributes } from "./components/Attributes"; const formFields = { cfg: "cfg", @@ -49,13 +43,13 @@ const validationSchema = Yup.object().shape({ } }, }), + [formFields.family]: Yup.string().required("Family is required."), }); -export default function UploadConfig() { +export default function UploadConfigView() { const auth = useContext(AuthContext); const navigate = useNavigate(); const searchParams = useSearchParams()[0]; - const [attributeModalOpen, setAttributeModalOpen] = useState(false); const formOptions = { resolver: yupResolver(validationSchema), @@ -76,15 +70,6 @@ export default function UploadConfig() { control, } = useForm(formOptions); - const { - fields: attributes, - append: appendAttribute, - remove: removeAttribute, - } = useFieldArray({ - control, - name: formFields.attributes, - }); - const handleParentClear = () => { if (searchParams.get("parent")) navigate("/config_upload"); setValue(formFields.parent, ""); @@ -113,17 +98,6 @@ export default function UploadConfig() { } }; - const onAttributeAdd = (key, value) => { - for (let attr of attributes) - if (attr.key === key && attr.value === value) { - // that key, value was added yet - setAttributeModalOpen(false); - return; - } - appendAttribute({ key, value }); - setAttributeModalOpen(false); - }; - return ( @@ -150,26 +124,7 @@ export default function UploadConfig() {
    - -
    -
    - -
    - -
    +
    + {auth.hasCapability(Capability.addingParents) && (
    @@ -227,45 +184,30 @@ export default function UploadConfig() {
    )} -
    -
    Attributes
    - setAttributeModalOpen(true)} - /> +
    +
    + +
    +
    - - - {attributes.map((attr, idx) => ( - - {attr.key} - - {typeof attr.value === "string" ? ( - attr.value - ) : ( -
    -                                                {"(object)"}{" "}
    -                                                {JSON.stringify(
    -                                                    attr.value,
    -                                                    null,
    -                                                    4
    -                                                )}
    -                                            
    - )} - - - removeAttribute(idx)} - /> - - - ))} -
    +
    - setAttributeModalOpen(false)} - onAdd={onAttributeAdd} - /> ); } diff --git a/mwdb/web/src/components/Upload/UploadFile.jsx b/mwdb/web/src/components/Upload/UploadFile.jsx deleted file mode 100644 index 0cac373ba..000000000 --- a/mwdb/web/src/components/Upload/UploadFile.jsx +++ /dev/null @@ -1,323 +0,0 @@ -import React, { useCallback, useContext, useState, useEffect } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { useDropzone } from "react-dropzone"; -import { toast } from "react-toastify"; - -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faUpload } from "@fortawesome/free-solid-svg-icons"; -import AttributesAddModal from "../AttributesAddModal"; - -import { api } from "@mwdb-web/commons/api"; -import { AuthContext, Capability } from "@mwdb-web/commons/auth"; -import { - Autocomplete, - DataTable, - View, - getErrorMessage, -} from "@mwdb-web/commons/ui"; -import { ConfigContext } from "@mwdb-web/commons/config"; -import { Extendable } from "@mwdb-web/commons/plugins"; - -function UploadDropzone(props) { - const onDrop = props.onDrop; - const { getRootProps, getInputProps, isDragActive, isDragReject } = - useDropzone({ - multiple: false, - onDrop: useCallback( - (acceptedFiles) => onDrop(acceptedFiles[0]), - [onDrop] - ), - }); - - const dropzoneClassName = isDragActive - ? "dropzone-active" - : isDragReject - ? "dropzone-reject" - : ""; - - return ( -
    - -
    -
    -
    - -   - {props.file ? ( - - {props.file.name} - {props.file.size} bytes - - ) : ( - Click here to upload - )} -
    -
    -
    -
    - ); -} - -export default function UploadFile() { - const auth = useContext(AuthContext); - const config = useContext(ConfigContext); - const fileUploadTimeout = config.config["file_upload_timeout"]; - const navigate = useNavigate(); - const searchParams = useSearchParams()[0]; - - const [file, setFile] = useState(null); - const [shareWith, setShareWith] = useState("default"); - const [group, setGroup] = useState(""); - const [groups, setGroups] = useState([]); - const [parent, setParent] = useState(""); - const [attributes, setAttributes] = useState([]); - const [attributeModalOpen, setAttributeModalOpen] = useState(false); - - const handleParentChange = (ev) => { - ev.preventDefault(); - setParent(ev.target.value); - }; - - const handleParentClear = () => { - if (searchParams.get("parent")) navigate("/file_upload"); - setParent(""); - }; - - const updateSharingMode = (ev) => { - setShareWith(ev.target.value); - setGroup(""); - }; - - const sharingModeToUploadParam = () => { - if (shareWith === "default") return "*"; - else if (shareWith === "public") return "public"; - else if (shareWith === "private") return "private"; - else if (shareWith === "single") return group; - }; - - const getSharingModeHint = () => { - if (shareWith === "default") - return `The sample and all related artifacts will be shared with all your workgroups`; - else if (shareWith === "public") - return `The sample will be added to the public feed, so everyone will see it.`; - else if (shareWith === "private") - return `The sample will be accessible only from your account.`; - else if (shareWith === "single") - return `The sample will be accessible only for you and chosen group.`; - }; - - const handleSubmit = async () => { - try { - let response = await api.uploadFile( - file, - searchParams.get("parent") || parent, - sharingModeToUploadParam(), - attributes, - fileUploadTimeout - ); - navigate("/file/" + response.data.sha256, { - replace: true, - }); - toast("File uploaded successfully.", { - type: "success", - }); - } catch (error) { - toast(getErrorMessage(error), { - type: "error", - }); - } - }; - - const onAttributeAdd = (key, value) => { - for (let attr of attributes) - if (attr.key === key && attr.value === value) { - // that key, value was added yet - setAttributeModalOpen(false); - return; - } - setAttributes([...attributes, { key, value }]); - setAttributeModalOpen(false); - }; - - const onAttributeRemove = (idx) => { - setAttributes([ - ...attributes.slice(0, idx), - ...attributes.slice(idx + 1), - ]); - }; - - useEffect(() => { - getGroups(); - }, [auth?.user?.login]); - - async function getGroups() { - try { - let response = await api.getShareInfo(); - let groups = response.data.groups; - groups.splice(groups.indexOf("public"), 1); - groups.splice(groups.indexOf(auth.user.login), 1); - setGroups(groups); - setShareWith(groups.length > 0 ? "default" : "private"); - } catch (error) { - toast(getErrorMessage(error), { - type: "error", - }); - } - } - - return ( - - -
    - setFile(data)} - /> -
    - {auth.hasCapability(Capability.addingParents) && ( -
    -
    - -
    - -
    - -
    -
    - )} -
    -
    - -
    - -
    - {getSharingModeHint()} -
    -
    - {shareWith === "single" && ( -
    - - item - .toLowerCase() - .indexOf( - group.toLowerCase() - ) !== -1 - )} - onChange={(value) => setGroup(value)} - className="form-control" - style={{ fontSize: "medium" }} - placeholder="Type group name..." - prependChildren - > -
    - -
    -
    -
    - )} -
    -
    Attributes
    - setAttributeModalOpen(true)} - /> -
    - - {attributes.map((attr, idx) => ( - - {attr.key} - - {typeof attr.value === "string" ? ( - attr.value - ) : ( -
    -                                                {"(object)"}{" "}
    -                                                {JSON.stringify(
    -                                                    attr.value,
    -                                                    null,
    -                                                    4
    -                                                )}
    -                                            
    - )} - - - - onAttributeRemove(idx) - } - /> - - - ))} -
    - -
    - -
    -
    - -
    - setAttributeModalOpen(false)} - onAdd={onAttributeAdd} - /> -
    - ); -} diff --git a/mwdb/web/src/components/Upload/UploadFileView.jsx b/mwdb/web/src/components/Upload/UploadFileView.jsx new file mode 100644 index 000000000..7651736d0 --- /dev/null +++ b/mwdb/web/src/components/Upload/UploadFileView.jsx @@ -0,0 +1,260 @@ +import React, { useContext, useState, useEffect } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { useForm, useFieldArray } from "react-hook-form"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; + +import { Attributes } from "./components/Attributes"; + +import { api } from "@mwdb-web/commons/api"; +import { AuthContext, Capability } from "@mwdb-web/commons/auth"; +import { + Autocomplete, + View, + getErrorMessage, + FormError, +} from "@mwdb-web/commons/ui"; +import { ConfigContext } from "@mwdb-web/commons/config"; +import { Extendable } from "@mwdb-web/commons/plugins"; +import { UploadDropzone } from "./components/UploadDropzone"; + +const formFields = { + file: "file", + parent: "parent", + shareWith: "shareWith", + group: "group", + attributes: "attributes", +}; + +const validationSchema = Yup.object().shape({ + [formFields.file]: Yup.mixed().required("File is required"), +}); + +export default function UploadFileView() { + const auth = useContext(AuthContext); + const config = useContext(ConfigContext); + const fileUploadTimeout = config.config["file_upload_timeout"]; + const navigate = useNavigate(); + const searchParams = useSearchParams()[0]; + const [groups, setGroups] = useState([]); + + const formOptions = { + resolver: yupResolver(validationSchema), + mode: "onSubmit", + reValidateMode: "onSubmit", + defaultValues: { + [formFields.file]: null, + [formFields.shareWith]: "", + [formFields.group]: "", + [formFields.parent]: searchParams.get("parent") || "", + [formFields.attributes]: [], + }, + }; + + const { + register, + setValue, + handleSubmit, + watch, + formState: { errors }, + control, + } = useForm(formOptions); + + const { file, shareWith, group } = watch(); + + const handleParentClear = () => { + if (searchParams.get("parent")) navigate("/file_upload"); + setValue(formFields.parent, ""); + }; + + const updateSharingMode = (ev) => { + setValue(formFields.shareWith, ev.target.value); + setValue(formFields.group, ""); + }; + + const sharingModeToUploadParam = (_shareWith) => { + if (_shareWith === "default") return "*"; + else if (_shareWith === "public") return "public"; + else if (_shareWith === "private") return "private"; + else if (_shareWith === "single") return group; + }; + + const getSharingModeHint = () => { + if (shareWith === "default") + return `The sample and all related artifacts will be shared with all your workgroups`; + else if (shareWith === "public") + return `The sample will be added to the public feed, so everyone will see it.`; + else if (shareWith === "private") + return `The sample will be accessible only from your account.`; + else if (shareWith === "single") + return `The sample will be accessible only for you and chosen group.`; + }; + + const onSubmit = async (values) => { + try { + const response = await api.uploadFile( + values[formFields.file], + searchParams.get("parent") || values[formFields.parent], + sharingModeToUploadParam(values[formFields.shareWith]), + values[formFields.attributes], + fileUploadTimeout + ); + navigate("/file/" + response.data.sha256, { + replace: true, + }); + toast("File uploaded successfully.", { + type: "success", + }); + } catch (error) { + toast(getErrorMessage(error), { + type: "error", + }); + } + }; + + useEffect(() => { + getGroups(); + }, [auth?.user?.login]); + + async function getGroups() { + try { + let response = await api.getShareInfo(); + let groups = response.data.groups; + groups.splice(groups.indexOf("public"), 1); + groups.splice(groups.indexOf(auth.user.login), 1); + + setGroups(groups); + setValue( + formFields.shareWith, + groups.length > 0 ? "default" : "private" + ); + } catch (error) { + toast(getErrorMessage(error), { + type: "error", + }); + } + } + + return ( + + +
    + { + setValue(formFields.file, data); + }} + /> + +
    + {auth.hasCapability(Capability.addingParents) && ( +
    +
    + +
    + +
    + +
    +
    + )} +
    +
    + +
    + +
    + {getSharingModeHint()} +
    +
    + {shareWith === "single" && ( +
    + + item + .toLowerCase() + .indexOf( + group.toLowerCase() + ) !== -1 + )} + onChange={(value) => + setValue(formFields.group, value) + } + className="form-control" + style={{ fontSize: "medium" }} + placeholder="Type group name..." + prependChildren + > +
    + +
    +
    +
    + )} + + +
    + +
    +
    + +
    +
    + ); +} diff --git a/mwdb/web/src/components/Upload/components/Attributes.jsx b/mwdb/web/src/components/Upload/components/Attributes.jsx new file mode 100644 index 000000000..243c1f985 --- /dev/null +++ b/mwdb/web/src/components/Upload/components/Attributes.jsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import { DataTable } from "@mwdb-web/commons/ui"; +import AttributesAddModal from "../../AttributesAddModal"; + +export function Attributes({ fields, append, remove }) { + const [attributeModalOpen, setAttributeModalOpen] = useState(false); + + const onAttributeAdd = (key, value) => { + for (let attr of fields) + if (attr.key === key && attr.value === value) { + // that key, value was added yet + setAttributeModalOpen(false); + return; + } + append({ key, value }); + setAttributeModalOpen(false); + }; + + return ( + <> +
    +
    Attributes
    + setAttributeModalOpen(true)} + /> +
    + + {fields.map((attr, idx) => ( + + {attr.key} + + {typeof attr.value === "string" ? ( + attr.value + ) : ( +
    +                                    {"(object)"}{" "}
    +                                    {JSON.stringify(attr.value, null, 4)}
    +                                
    + )} + + + remove(idx)} + /> + + + ))} +
    + setAttributeModalOpen(false)} + onAdd={onAttributeAdd} + /> + + ); +} diff --git a/mwdb/web/src/components/Upload/components/UploadDropzone.jsx b/mwdb/web/src/components/Upload/components/UploadDropzone.jsx new file mode 100644 index 000000000..024b79b20 --- /dev/null +++ b/mwdb/web/src/components/Upload/components/UploadDropzone.jsx @@ -0,0 +1,49 @@ +import { useCallback } from "react"; +import { useDropzone } from "react-dropzone"; +import { faUpload } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export function UploadDropzone(props) { + const onDrop = props.onDrop; + const { getRootProps, getInputProps, isDragActive, isDragReject } = + useDropzone({ + multiple: false, + onDrop: useCallback( + (acceptedFiles) => { + onDrop(acceptedFiles[0]); + }, + [onDrop] + ), + }); + + const dropzoneClassName = isDragActive + ? "dropzone-active" + : isDragReject + ? "dropzone-reject" + : ""; + + return ( +
    + +
    +
    +
    + +   + {props.file ? ( + + {props.file.name} - {props.file.size} bytes + + ) : ( + Click here to upload + )} +
    +
    +
    +
    + ); +} From ad3434665f6de415e494d41314ca6886f44c5281 Mon Sep 17 00:00:00 2001 From: postrowinski Date: Mon, 24 Apr 2023 13:28:52 +0200 Subject: [PATCH 06/54] add upload blob form --- mwdb/web/src/App.jsx | 3 +- mwdb/web/src/commons/api/index.jsx | 17 +- .../src/components/Upload/UploadBlobView.jsx | 273 ++++++++++++++++++ .../components/Upload/UploadConfigView.jsx | 178 ++++++------ .../src/components/Upload/UploadFileView.jsx | 82 ++---- .../{components => common}/Attributes.jsx | 0 .../{components => common}/UploadDropzone.jsx | 0 .../components/Upload/common/helpers/index.js | 19 ++ .../Upload/common/hooks/useGroup.js | 34 +++ 9 files changed, 454 insertions(+), 152 deletions(-) create mode 100644 mwdb/web/src/components/Upload/UploadBlobView.jsx rename mwdb/web/src/components/Upload/{components => common}/Attributes.jsx (100%) rename mwdb/web/src/components/Upload/{components => common}/UploadDropzone.jsx (100%) create mode 100644 mwdb/web/src/components/Upload/common/helpers/index.js create mode 100644 mwdb/web/src/components/Upload/common/hooks/useGroup.js diff --git a/mwdb/web/src/App.jsx b/mwdb/web/src/App.jsx index 713976573..c3004505f 100644 --- a/mwdb/web/src/App.jsx +++ b/mwdb/web/src/App.jsx @@ -19,6 +19,7 @@ import ShowTextBlob from "./components/ShowTextBlob"; import DiffTextBlob from "./components/DiffTextBlob"; import UploadFileView from "./components/Upload/UploadFileView"; import UploadConfigView from "./components/Upload/UploadConfigView"; +import UploadBlobView from "./components/Upload/UploadBlobView"; import UserLogin from "./components/UserLogin"; import UserRegister from "./components/UserRegister"; import UserSetPassword from "./components/UserSetPassword"; @@ -121,7 +122,7 @@ function AppRoutes() { path="blob_upload" element={ - + } /> diff --git a/mwdb/web/src/commons/api/index.jsx b/mwdb/web/src/commons/api/index.jsx index d50024a1e..ed0ebc83b 100644 --- a/mwdb/web/src/commons/api/index.jsx +++ b/mwdb/web/src/commons/api/index.jsx @@ -430,20 +430,32 @@ async function requestZipFileDownloadLink(id) { return `${baseURL}/file/${id}/download/zip?token=${response.data.token}`; } -function uploadFile(file, parent, upload_as, attributes, fileUploadTimeout) { +function uploadFile(body) { + const { file, parent, shareWith, attributes, fileUploadTimeout } = body; let formData = new FormData(); formData.append("file", file); formData.append( "options", JSON.stringify({ parent: parent || null, - upload_as: upload_as, + upload_as: shareWith, attributes: attributes, }) ); return axios.post(`/file`, formData, { timeout: fileUploadTimeout }); } +function uploadBlob(body) { + const { name, shareWith, parent, type } = body; + return axios.post(`/blob`, { + ...body, + blob_name: name, + blob_type: type, + upload_as: shareWith, + parent: parent || null, + }); +} + function uploadConfig(body) { return axios.post("/config", body); } @@ -632,6 +644,7 @@ export const api = { requestZipFileDownloadLink, uploadFile, uploadConfig, + uploadBlob, getRemoteNames, pushObjectRemote, pullObjectRemote, diff --git a/mwdb/web/src/components/Upload/UploadBlobView.jsx b/mwdb/web/src/components/Upload/UploadBlobView.jsx new file mode 100644 index 000000000..2e709e3e0 --- /dev/null +++ b/mwdb/web/src/components/Upload/UploadBlobView.jsx @@ -0,0 +1,273 @@ +import React, { useContext } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { useForm, useFieldArray } from "react-hook-form"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; + +import { Attributes } from "./common/Attributes"; + +import { api } from "@mwdb-web/commons/api"; +import { AuthContext, Capability } from "@mwdb-web/commons/auth"; +import { + Autocomplete, + View, + getErrorMessage, + FormError, +} from "@mwdb-web/commons/ui"; +import { ConfigContext } from "@mwdb-web/commons/config"; +import { Extendable } from "@mwdb-web/commons/plugins"; +import { useGroup } from "./common/hooks/useGroup"; +import { getSharingModeHint, sharingModeToUploadParam } from "./common/helpers"; + +const formFields = { + content: "content", + type: "type", + name: "name", + parent: "parent", + shareWith: "shareWith", + group: "group", + attributes: "attributes", +}; + +const validationSchema = Yup.object().shape({ + [formFields.content]: Yup.string().required("Content is required"), + [formFields.type]: Yup.string().required("Type is required"), + [formFields.name]: Yup.string().required("Name is required"), +}); + +export default function UploadBlobView() { + const searchParams = useSearchParams()[0]; + const formOptions = { + resolver: yupResolver(validationSchema), + mode: "onSubmit", + reValidateMode: "onSubmit", + defaultValues: { + [formFields.content]: "", + [formFields.shareWith]: "", + [formFields.group]: "", + [formFields.parent]: searchParams.get("parent") || "", + [formFields.attributes]: [], + }, + }; + + const { + register, + setValue, + handleSubmit, + watch, + formState: { errors }, + control, + } = useForm(formOptions); + + const auth = useContext(AuthContext); + const config = useContext(ConfigContext); + const navigate = useNavigate(); + + const { groups } = useGroup(setValue, formFields.shareWith); + + const { shareWith, group } = watch(); + + const handleParentClear = () => { + if (searchParams.get("parent")) navigate("/blob_upload"); + setValue(formFields.parent, ""); + }; + + const updateSharingMode = (ev) => { + setValue(formFields.shareWith, ev.target.value); + setValue(formFields.group, ""); + }; + + const onSubmit = async (values) => { + try { + const body = { + ...values, + parent: searchParams.get("parent") || values[formFields.parent], + shareWith: sharingModeToUploadParam( + values[formFields.shareWith], + values[formFields.group] + ), + }; + const response = await api.uploadBlob(body); + navigate("/blob/" + response.data.id, { + replace: true, + }); + toast("Blob uploaded successfully.", { + type: "success", + }); + } catch (error) { + toast(getErrorMessage(error), { + type: "error", + }); + } + }; + + return ( + + +

    Blob upload

    +
    +
    +
    + +
    +