= ({
18 | fileName,
19 | onDownload,
20 | onDelete,
21 | thumbnail,
22 | file,
23 | }) => {
24 | const [icon, setIcon] = useState("");
25 | const [loading, setLoading] = useState(true);
26 | const [openPreview, setOpenPreview] = useState(false);
27 | const [isImage, setIsImage] = useState(false);
28 | const [hdImage, setHdImage] = useState("");
29 | const params = useParams();
30 |
31 | const handlePreviewClick = () => {
32 | const hdFileId = file?.content?.file_id;
33 | if (!hdImage && hdFileId) {
34 | getDownloadURL(hdFileId).then((image) => {
35 | setHdImage(image.url);
36 | });
37 | }
38 | setOpenPreview(true);
39 | };
40 |
41 | useEffect(() => {
42 | if (file?.content?.mime_type?.includes("image")) {
43 | setIsImage(true);
44 | }
45 | if (thumbnail) {
46 | getDownloadURL(thumbnail)
47 | .then((image) => {
48 | setIcon(image.url);
49 | })
50 | .finally(() => {
51 | setLoading(false);
52 | });
53 | } else {
54 | setLoading(false);
55 | }
56 | }, [file, thumbnail]);
57 |
58 | useEffect(() => {
59 | setTimeout(() => {
60 | if (history.location.hash !== "#preview") {
61 | setOpenPreview(false);
62 | }
63 | });
64 | }, [params]);
65 |
66 | useEffect(() => {
67 | // Adding navigation params for preview
68 | if (openPreview) {
69 | history.push(
70 | history.location.pathname +
71 | history.location.search +
72 | "#preview"
73 | );
74 | }
75 | }, [openPreview])
76 |
77 |
78 | return (
79 |
80 |
81 | {loading ? (
82 |
88 | ) : (
89 |
103 | ),
104 | visible: openPreview,
105 | onVisibleChange(value) {
106 | if (!value) {
107 | setOpenPreview(false);
108 | history.back();
109 | }
110 | },
111 | }}
112 | />
113 | )}
114 |
115 | {shortenFileName(fileName, "file", isMobile ? 12 : 20)}
116 |
117 | );
118 | };
119 |
120 | export default File;
121 |
--------------------------------------------------------------------------------
/ui/src/components/folder/fileMask.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Modal } from "antd";
2 | import { DownloadOutlined, EyeOutlined, DeleteOutlined } from "@ant-design/icons";
3 | import { useState } from "react";
4 |
5 | const btnWidth = 30;
6 | const FileMask = ({ onPreview, onDownload, fileSize, onDelete }: any) => {
7 | const [deleteModalOpen, setDeleteModalOpen] = useState(false);
8 | return (
9 |
10 | {onPreview && (
11 | <>
12 | }
15 | onClick={onPreview}
16 | style={{ marginRight: 10, width: btnWidth }}
17 | >
18 | >
19 | )}
20 | }
23 | style={{ marginRight: 10, width: btnWidth }}
24 | onClick={onDownload}
25 | >
26 |
33 |
34 |
35 |
36 | {fileSize}
37 | setDeleteModalOpen(false)}
42 | destroyOnClose
43 | footer={[
44 | ,
47 | ,
55 | ]}
56 | >
57 | Are you sure you want to delete this file ?
58 |
59 |
60 | );
61 | };
62 |
63 | export default FileMask;
64 |
--------------------------------------------------------------------------------
/ui/src/components/folder/folder.css:
--------------------------------------------------------------------------------
1 | .folder{
2 | display: inline-block;
3 | padding: 10px;
4 | border: 1px solid rgb(218 220 224);
5 | width: 200px;
6 | border-radius: 6px;
7 | margin: 10px 10px 10px 0;
8 | cursor: pointer;
9 | font-weight: 500;
10 | user-select: none;
11 | background: #9fc5ea;
12 | }
13 | .folder:hover{
14 | background: #e7e7e7;
15 | }
16 |
17 | .folder p {
18 | text-overflow: ellipsis;
19 | white-space: nowrap;
20 | overflow: hidden;
21 | margin: 5px;
22 | display: inline-block;
23 | vertical-align: middle;
24 | }
25 | .folder span{
26 | vertical-align: middle;
27 | }
28 |
29 | .folderIcon{
30 | margin-right: 10px;
31 | font-size: 25px;
32 | vertical-align: sub;
33 |
34 | }
35 |
36 | .file{
37 | display: inline-block;
38 | padding: 10px;
39 | border: 1px solid rgb(218 220 224);
40 | width: 230px;
41 | border-radius: 6px;
42 | margin: 10px 10px 10px 0;
43 | cursor: pointer;
44 | font-weight: 500;
45 | user-select: none;
46 | text-align: center;
47 | background: white;
48 | }
49 |
50 | .mask{
51 | background-color: black;
52 | }
53 |
54 | .file img {
55 | object-fit: cover;
56 | width: 210px;
57 | }
58 |
59 | .file:hover{
60 | background: #e7e7e7;
61 | }
62 | .file p {
63 | margin-bottom: 0;
64 | margin-top: 10px;
65 | text-overflow: ellipsis;
66 | white-space: nowrap;
67 | overflow: hidden;
68 | margin: 5px;
69 | }
70 |
71 | .fileIcon{
72 | margin-right: 10px;
73 | font-size: 100px;
74 | font-weight: normal;
75 | vertical-align: sub;
76 | padding-top: 50px;
77 | padding-bottom: 50px;
78 | }
79 |
80 | .imageSkeleton{
81 | padding: 23px 0;
82 | }
83 |
84 | .imageSkeleton div {
85 | width: 130px !important;
86 | height: 154px !important;
87 | }
88 |
89 | /* Mobile styles */
90 |
91 | .file-mobile{
92 | width: 150px;
93 | padding: 3px;
94 | }
95 |
96 | .file-mobile img {
97 | width: 140px;
98 | height: 120px;
99 | }
100 |
101 | .file-mobile div > .ant-image-mask {
102 | opacity: 1;
103 | }
104 |
105 | .folder-mobile{
106 | width: 120px;
107 | padding: 3px;
108 | }
109 |
110 | .folder-mobile img {
111 | width: 114px;
112 | height: 120px;
113 | }
114 |
115 | .folder-mobile span {
116 | width: 114px;
117 | object-fit: cover;
118 | padding-top: 15px;
119 | }
120 |
121 | .imageSkeleton-mobile div {
122 | width: 100px !important;
123 | height: 90px !important;
124 | }
--------------------------------------------------------------------------------
/ui/src/components/folder/folder.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FolderOutlined } from "@ant-design/icons";
3 | import { isMobile } from "react-device-detect";
4 | import "./folder.css";
5 | import { shortenFileName } from "../../services/common";
6 |
7 | interface FolderProps {
8 | folderName: string;
9 | onClick: any;
10 | }
11 | const Folder: React.FC = ({ folderName, onClick }) => {
12 | return (
13 |
18 |
19 |
20 |
21 | {shortenFileName(folderName, "folder", isMobile ? 12 : 20)}
22 |
23 | );
24 | };
25 |
26 | export default Folder;
27 |
--------------------------------------------------------------------------------
/ui/src/components/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Divider, Layout, Skeleton, Tooltip } from "antd";
2 | import { Content, Footer, Header } from "antd/lib/layout/layout";
3 | import React from "react";
4 | import Navigator from "./navigator";
5 | import { PoweroffOutlined } from "@ant-design/icons";
6 | import { history } from "../services/common";
7 | import { isMobile } from "react-device-detect";
8 |
9 | const LayoutComponent: React.FC = ({
10 | actions,
11 | children,
12 | path,
13 | loading,
14 | }) => {
15 | const handleLogout = () => {
16 | localStorage.removeItem("accessToken");
17 | history.push("/login");
18 | };
19 | return (
20 |
21 |
38 |
41 |
42 | {actions}
43 |
44 | {loading ? : children}
45 |
46 |
47 |
51 |
52 | );
53 | };
54 |
55 | export default LayoutComponent;
56 |
--------------------------------------------------------------------------------
/ui/src/components/navigator.tsx:
--------------------------------------------------------------------------------
1 | import { HomeOutlined, FolderOutlined } from "@ant-design/icons";
2 | import { Breadcrumb } from "antd";
3 | import React from "react";
4 | import { useSearchParams } from "react-router-dom";
5 | import "./components.css";
6 |
7 | interface NavigatorProps {
8 | path: string;
9 | }
10 | const Navigator: React.FC = ({ path }) => {
11 | const [, setSearchParams] = useSearchParams();
12 |
13 | const pathList = path.slice(1).split("/");
14 | const tail = pathList[pathList.length - 1];
15 | const middleItems = pathList.slice(1, -1);
16 |
17 | const handleClick = (index: number) => {
18 | const completePath = "/" + pathList.slice(0, index + 1).join("/");
19 | setSearchParams({ path: completePath });
20 | };
21 |
22 | return (
23 |
24 | handleClick(0)}>
25 |
26 | Home
27 |
28 | {middleItems.map((item, index) => {
29 | return (
30 | handleClick(index + 1)}
34 | >
35 |
36 | {item}
37 |
38 | );
39 | })}
40 | {tail !== "root" && (
41 |
42 |
43 | {tail}
44 |
45 | )}
46 |
47 | );
48 | };
49 |
50 | export default Navigator;
51 |
--------------------------------------------------------------------------------
/ui/src/components/uploader/styles.css:
--------------------------------------------------------------------------------
1 | .dzu-dropzone {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | width: 100%;
6 | min-height: 50vh;
7 | overflow: scroll;
8 | margin: 0 auto;
9 | position: relative;
10 | box-sizing: border-box;
11 | transition: all .15s linear;
12 | border: 2px dashed #d9d9d9;
13 | border-radius: 10px;
14 | }
15 |
16 | .dzu-dropzoneActive {
17 | background-color: #DEEBFF;
18 | border-color: #2484FF;
19 | }
20 |
21 | .dzu-dropzoneDisabled {
22 | opacity: 0.5;
23 | }
24 |
25 | .dzu-dropzoneDisabled *:hover {
26 | cursor: unset;
27 | }
28 |
29 | .dzu-input {
30 | display: none;
31 | }
32 |
33 | .dzu-inputLabel {
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | position: absolute;
38 | top: 0;
39 | bottom: 0;
40 | left: 0;
41 | right: 0;
42 | font-family: 'Helvetica', sans-serif;
43 | font-size: 20px;
44 | font-weight: 600;
45 | color: #2484FF;
46 | -moz-osx-font-smoothing: grayscale;
47 | -webkit-font-smoothing: antialiased;
48 | cursor: pointer;
49 | }
50 |
51 | .dzu-inputLabelWithFiles {
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | align-self: flex-start;
56 | padding: 0 14px;
57 | min-height: 32px;
58 | background-color: #E6E6E6;
59 | color: #2484FF;
60 | border: none;
61 | font-family: 'Helvetica', sans-serif;
62 | border-radius: 4px;
63 | font-size: 14px;
64 | font-weight: 600;
65 | margin-top: 20px;
66 | margin-left: 3%;
67 | margin-bottom: 10px;
68 | -moz-osx-font-smoothing: grayscale;
69 | -webkit-font-smoothing: antialiased;
70 | cursor: pointer;
71 | }
72 |
73 | .dzu-previewContainer {
74 | padding: 40px 3%;
75 | display: flex;
76 | flex-direction: row;
77 | align-items: center;
78 | justify-content: space-between;
79 | position: relative;
80 | width: 100%;
81 | min-height: 60px;
82 | z-index: 1;
83 | border-bottom: 1px solid #ECECEC;
84 | box-sizing: border-box;
85 | }
86 |
87 | .dzu-previewStatusContainer {
88 | display: flex;
89 | align-items: center;
90 | }
91 |
92 | .dzu-previewFileName {
93 | font-family: 'Helvetica', sans-serif;
94 | font-size: 14px;
95 | font-weight: 400;
96 | color: #333333;
97 | width: 40px;
98 | }
99 |
100 | .dzu-previewImage {
101 | max-height: 40px;
102 | width: 30px;
103 | border-radius: 4px;
104 | object-fit: cover;
105 | }
106 |
107 | .dzu-previewButton {
108 | background-size: 14px 14px;
109 | background-position: center;
110 | background-repeat: no-repeat;
111 | width: 14px;
112 | height: 14px;
113 | cursor: pointer;
114 | opacity: 0.9;
115 | margin: 0 0 2px 10px;
116 | }
117 |
118 | .dzu-submitButtonContainer {
119 | margin: 24px 0;
120 | z-index: 1;
121 | }
122 |
123 | .dzu-submitButton {
124 | padding: 0 14px;
125 | min-height: 32px;
126 | background-color: #2484FF;
127 | border: none;
128 | border-radius: 4px;
129 | font-family: 'Helvetica', sans-serif;
130 | font-size: 14px;
131 | font-weight: 600;
132 | color: #FFF;
133 | -moz-osx-font-smoothing: grayscale;
134 | -webkit-font-smoothing: antialiased;
135 | cursor: pointer;
136 | }
137 |
138 | .dzu-submitButton:disabled {
139 | background-color: #E6E6E6;
140 | color: #333333;
141 | cursor: unset;
142 | }
143 |
--------------------------------------------------------------------------------
/ui/src/components/uploader/uploader.tsx:
--------------------------------------------------------------------------------
1 | import "./styles.css";
2 | import Dropzone from "react-dropzone-uploader";
3 | import { isMobile } from "react-device-detect";
4 | import { UPLOAD_FILE } from "../../services/endpoints";
5 | import { getBaseURL } from "../../services/common";
6 |
7 | interface UploaderProps {
8 | path: string;
9 | done: any;
10 | }
11 |
12 | const Uploader: React.FC = ({ path, done }) => {
13 | const getUploadParams = ({ file, meta }: any) => {
14 | const body = new FormData();
15 | body.append("file", file);
16 | body.append("path", path);
17 | return {
18 | url: `${getBaseURL()}${UPLOAD_FILE}`,
19 | body,
20 | headers: {
21 | Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
22 | },
23 | };
24 | };
25 |
26 | const handleChangeStatus = ({ meta, file, xhr }: any, status: any) => {
27 | if (status === "done") {
28 | done(JSON.parse(xhr.response));
29 | }
30 | };
31 |
32 | return (
33 |
38 | );
39 | };
40 | export default Uploader;
41 |
--------------------------------------------------------------------------------
/ui/src/index.css:
--------------------------------------------------------------------------------
1 | @import '~antd/dist/antd.css';
2 | @import url('https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500&display=swap');
3 |
4 | body {
5 | margin: 0;
6 | font-family: 'Ubuntu', sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/ui/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById("root") as HTMLElement
9 | );
10 | root.render(
11 | //
12 |
13 | //
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/ui/src/pages/home/home.css:
--------------------------------------------------------------------------------
1 | /* tile uploaded pictures */
2 | .upload-list-inline .ant-upload-list-item {
3 | float: left;
4 | width: 200px;
5 | margin-right: 8px;
6 | }
7 |
8 | .upload-list-inline [class*='-upload-list-rtl'] .ant-upload-list-item {
9 | float: right;
10 | }
11 |
12 | .header-btns > .ant-btn > span {
13 | padding-bottom: 18px;
14 | display: inline-block;
15 | font-size: 30px;
16 | }
17 |
18 | .center {
19 | text-align: center;
20 | }
21 | .header-btns{
22 | text-align: center;
23 | }
24 |
25 | .header-btns button {
26 | height: 40px;
27 | }
--------------------------------------------------------------------------------
/ui/src/pages/home/home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from "react";
2 | import { useParams, useSearchParams } from "react-router-dom";
3 |
4 | import LayoutComponent from "../../components/layout";
5 | import { listFolder } from "../../services/folder";
6 | import "../../components/components.css";
7 | import "./home.css";
8 | import { Divider, Empty } from "antd";
9 | import AddFolder from "./modals/addFolderModal";
10 |
11 | import FolderComponent from "../../components/folder/folder";
12 | import FileComponent from "../../components/folder/file";
13 | import { deleteFile, getDownloadURL } from "../../services/file";
14 | import UploadFile from "./modals/uploadFileModal";
15 | import DeleteFolder from "./modals/deleteFolderModal";
16 | import { isMobile } from "react-device-detect";
17 | import { showErrorMessage } from "../../services/common";
18 | interface File {
19 | fileName: string;
20 | content: any;
21 | _id: string;
22 | }
23 | interface Folder {
24 | folderName: string;
25 | folders: Folder[];
26 | path: string;
27 | files: File[];
28 | }
29 |
30 | const Home: React.FC = () => {
31 | const [searchParams, setSearchParams] = useSearchParams();
32 | const [folderInfo, setFolderInfo] = useState();
33 | const [folderLoading, setFolderLoading] = useState(false);
34 | const [previousPath, setPreviousPath] = useState("");
35 | const params = useParams();
36 |
37 |
38 | const getCurrentPath = useCallback(
39 | () => {
40 | return searchParams.get("path") || "/root";
41 | },
42 | [searchParams],
43 | )
44 |
45 | const handleFolderClick = (path: string) => {
46 | setSearchParams({ path });
47 | };
48 |
49 | const handleDownload = async (file: File) => {
50 | const fileId = file.content.file_id;
51 | const response = await getDownloadURL(fileId);
52 | window.open(response.url, "_self");
53 | };
54 |
55 | const handleDelete = async (file: File) => {
56 | try {
57 | const path = getCurrentPath();
58 | console.log(file, path);
59 | await deleteFile(path, file.content.file_id, file.content.message_id);
60 | if (folderInfo) {
61 | const files = [...folderInfo?.files];
62 | files.splice(files?.findIndex(f => f === file), 1);
63 | setFolderInfo({ ...folderInfo, files });
64 | }
65 | } catch (e) {
66 | showErrorMessage("An error occurred", "Error while deleting the file!")
67 | }
68 | };
69 |
70 |
71 | const updateFolder = (path: string) => {
72 | setFolderLoading(true);
73 | listFolder(path)
74 | .then((data) => {
75 | setFolderInfo(data as any);
76 | })
77 | .finally(() => {
78 | setFolderLoading(false);
79 | });
80 | }
81 |
82 | useEffect(() => {
83 | const path = getCurrentPath();
84 | if (previousPath !== path) {
85 | setPreviousPath(path);
86 | updateFolder(path);
87 | }
88 | }, [params, getCurrentPath, previousPath]);
89 |
90 | return (
91 |
96 |
97 |
98 |
99 |
100 | }
101 | >
102 | {!folderInfo?.folders?.length && !folderInfo?.files?.length && (
103 |
107 | )}
108 | {!!folderInfo?.folders?.length && }
109 |
110 | {folderInfo?.folders?.map((folder) => (
111 | handleFolderClick(folder.path)}
114 | folderName={folder.folderName}
115 | />
116 | ))}
117 |
118 |
119 | {!!folderInfo?.files?.length && }
120 |
121 | {folderInfo?.files?.map((file) => handleDownload(file)}
124 | onDelete={() => handleDelete(file)}
125 | fileName={file?.fileName}
126 | thumbnail={file?.content?.thumb?.file_id}
127 | file={file}
128 | />
129 | )}
130 |
131 |
132 | );
133 | };
134 |
135 | export default Home;
136 |
--------------------------------------------------------------------------------
/ui/src/pages/home/modals/addFolderModal.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Input, Modal } from "antd";
2 | import React, { useState } from "react";
3 | import { FolderAddOutlined } from "@ant-design/icons";
4 | import { useSearchParams } from "react-router-dom";
5 | import { makeFolder } from "../../../services/folder";
6 | import { isMobile } from "react-device-detect";
7 |
8 | interface AddFolderProps {
9 | updateList: any;
10 | }
11 |
12 | const AddFolder: React.FC = ({ updateList }) => {
13 | const [isModalOpen, setIsModalOpen] = useState(false);
14 | const [searchParams] = useSearchParams();
15 | const [newFolderName, setNewFolderName] = useState("");
16 | const [creating, setCreating] = useState(false);
17 |
18 | const showModal = () => {
19 | setIsModalOpen(true);
20 | };
21 |
22 | const handleOk = async () => {
23 | console.log(newFolderName);
24 | const path = searchParams.get("path");
25 | if (path) {
26 | try {
27 | setCreating(true);
28 | const response = await makeFolder(path, newFolderName);
29 | console.log(response);
30 | updateList(response);
31 | setIsModalOpen(false);
32 | } catch (e) {
33 | } finally {
34 | setCreating(false);
35 | }
36 | }
37 | };
38 |
39 | const handleCancel = () => {
40 | setIsModalOpen(false);
41 | };
42 |
43 | return (
44 | <>
45 |
49 |
57 | Cancel
58 | ,
59 | ,
68 | ]}
69 | >
70 | Location: '{searchParams.get("path")}'
71 | setNewFolderName(e.target.value)}
74 | />
75 |
76 | >
77 | );
78 | };
79 |
80 | export default AddFolder;
81 |
--------------------------------------------------------------------------------
/ui/src/pages/home/modals/deleteFolderModal.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Input, Modal } from "antd";
2 | import React, { useState } from "react";
3 | import { DeleteOutlined } from "@ant-design/icons";
4 | import { useSearchParams } from "react-router-dom";
5 | import { deleteFolder } from "../../../services/folder";
6 | import { isMobile } from "react-device-detect";
7 | interface DeleteFolderProps {
8 | updateList: any;
9 | }
10 |
11 | const DeleteFolder: React.FC = ({ updateList }) => {
12 | const [isModalOpen, setIsModalOpen] = useState(false);
13 | const [searchParams, setSearchParams] = useSearchParams();
14 | const [confirmationPath, setConfirmationPath] = useState("");
15 | const [deleting, setDeleting] = useState(false);
16 |
17 | const showModal = () => {
18 | setIsModalOpen(true);
19 | };
20 |
21 | const handleOk = async () => {
22 | const path = searchParams.get("path");
23 | if (path) {
24 | try {
25 | setDeleting(true);
26 | const response = await deleteFolder(path);
27 | const parentPath = path.split("/").slice(0, -1).join("/");
28 | console.log(response, parentPath);
29 | setSearchParams({ path: parentPath });
30 | setIsModalOpen(false);
31 | setConfirmationPath("");
32 | } catch (e) {
33 | } finally {
34 | setDeleting(false);
35 | }
36 | }
37 | };
38 |
39 | const handleCancel = () => {
40 | setIsModalOpen(false);
41 | };
42 |
43 | return (
44 | <>
45 |
54 |
62 | Cancel
63 | ,
64 | ,
74 | ]}
75 | >
76 |
77 | Are you sure you want to delete this folder and its child folders ?
78 |
79 |
80 | Type '{searchParams.get("path")}' to confirm delete
81 |
82 | setConfirmationPath(e.target.value)}
85 | />
86 |
87 | >
88 | );
89 | };
90 |
91 | export default DeleteFolder;
92 |
--------------------------------------------------------------------------------
/ui/src/pages/home/modals/uploadFileModal.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Modal } from "antd";
2 | import React, { useState } from "react";
3 | import { UploadOutlined } from "@ant-design/icons";
4 | import { useSearchParams } from "react-router-dom";
5 | import Uploader from "../../../components/uploader/uploader";
6 | import { isMobile } from "react-device-detect";
7 |
8 | interface UploadFileProps {
9 | updateList?: any;
10 | }
11 |
12 | const UploadFile: React.FC = ({ updateList }) => {
13 | const [isModalOpen, setIsModalOpen] = useState(false);
14 | const [searchParams] = useSearchParams();
15 |
16 | const showModal = () => {
17 | setIsModalOpen(true);
18 | };
19 |
20 | const handleCancel = () => {
21 | setIsModalOpen(false);
22 | };
23 |
24 | return (
25 | <>
26 |
30 |
38 |
42 |
43 | >
44 | );
45 | };
46 |
47 | export default UploadFile;
48 |
--------------------------------------------------------------------------------
/ui/src/pages/login/login.css:
--------------------------------------------------------------------------------
1 | .form-border{
2 | margin: 0 20px;
3 | margin-top: calc(45vh - 180px);
4 | padding: 41px 50px;
5 | border: 1px solid #d9d9d9;
6 | border-radius: 15px;
7 | text-align: center;
8 | }
9 |
10 | .title {
11 | font-weight: bold;
12 | font-size: 25px;
13 | text-align: center;
14 | margin-bottom: 18px;
15 | }
--------------------------------------------------------------------------------
/ui/src/pages/login/login.tsx:
--------------------------------------------------------------------------------
1 | import { LockOutlined, UserOutlined } from "@ant-design/icons";
2 | import { Button, Col, Form, Input, Row } from "antd";
3 | import React from "react";
4 | import { Link } from "react-router-dom";
5 | import { handleLogin } from "../../services/user";
6 | import { isMobile } from "react-device-detect";
7 |
8 | import "./login.css";
9 |
10 | const Login: React.FC = () => {
11 | return (
12 |
13 |
14 |
25 | } placeholder="Username" />
26 |
27 |
31 | }
33 | type="password"
34 | placeholder="Password"
35 | />
36 |
37 |
38 |
39 |
47 |
48 | Register
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default Login;
56 |
--------------------------------------------------------------------------------
/ui/src/pages/login/register.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | LockOutlined,
3 | UserOutlined,
4 | ClusterOutlined,
5 | TeamOutlined,
6 | } from "@ant-design/icons";
7 | import { Button, Col, Form, Input, Row } from "antd";
8 | import React from "react";
9 | import { Link } from "react-router-dom";
10 | import { handleRegistration } from "../../services/user";
11 | import { isMobile } from "react-device-detect";
12 |
13 | import "./login.css";
14 |
15 | const Register: React.FC = () => {
16 | return (
17 |
18 |
19 |
31 | } placeholder="Username" />
32 |
33 |
34 |
38 | }
40 | type="password"
41 | placeholder="Password"
42 | />
43 |
44 |
45 |
51 | }
53 | placeholder="Telegram Bot Token"
54 | />
55 |
56 |
57 |
63 | } placeholder="Telegram Group ID" />
64 |
65 |
66 |
67 |
75 |
76 | Login
77 |
78 |
85 | Help
86 |
87 |
88 |
89 |
90 | );
91 | };
92 |
93 | export default Register;
94 |
--------------------------------------------------------------------------------
/ui/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/ui/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/ui/src/services/common.ts:
--------------------------------------------------------------------------------
1 | import { notification } from "antd";
2 | import { message } from "antd";
3 | import axios from "axios";
4 | import { createBrowserHistory } from "history";
5 |
6 | console.log("Env:", process.env.NODE_ENV);
7 | type RequestMethod = "GET" | "POST";
8 | interface RequestData {
9 | body?: any;
10 | queryParams?: any;
11 | }
12 |
13 | let cachedBaseURL: string;
14 |
15 | const getBaseURLPromise = async () => {
16 | if (cachedBaseURL) {
17 | return cachedBaseURL;
18 | }
19 | const deployments = ["", "http://localhost:3000"];
20 |
21 | return new Promise(async (resolve, reject) => {
22 | for (let URL of deployments) {
23 | try {
24 | await fetch(`${URL}/api/v1/healthy`);
25 | cachedBaseURL = URL;
26 | console.log("Server Found: ", URL);
27 | resolve(URL);
28 | break;
29 | } catch (e) {
30 | console.log(`${URL} not available!`, e);
31 | }
32 | }
33 |
34 | reject("No deployments found!");
35 | });
36 | };
37 |
38 | export const getBaseURL = () => cachedBaseURL;
39 |
40 | export const makeRequest = async (
41 | url: string,
42 | reqMethod: RequestMethod,
43 | reqData?: RequestData,
44 | onUploadProgress?: any,
45 | onDownloadProgress?: any
46 | ) => {
47 | try {
48 | await getBaseURLPromise();
49 | const response = await axios({
50 | headers: {
51 | Authorization: `bearer ${localStorage.getItem("accessToken")}`,
52 | },
53 | baseURL: cachedBaseURL,
54 | url,
55 | method: reqMethod,
56 | data: reqData?.body,
57 | params: reqData?.queryParams,
58 | onUploadProgress,
59 | onDownloadProgress,
60 | });
61 | return response;
62 | } catch (e: any) {
63 | if (e.response.status === 401) {
64 | history.push("/login");
65 | }
66 | return {} as any;
67 | }
68 | };
69 |
70 | export const showErrorMessage = (title: string, description: string) => {
71 | notification.error({
72 | message: title,
73 | description,
74 | });
75 | };
76 |
77 | export const history = createBrowserHistory();
78 |
79 | export const isLoggedIn = () => {
80 | return !!localStorage.getItem("accessToken");
81 | };
82 |
83 | export const shortenFileName = (
84 | fileName: string,
85 | type: string = "file",
86 | length: number = 15
87 | ) => {
88 | const file = fileName.substring(0, length);
89 | if (type === "folder")
90 | return `${file}${fileName.length > length ? "..." : ""}`;
91 | try {
92 | const fileParts = fileName.split(".");
93 | const [extension] = fileParts.slice(-1);
94 | const fileNameWithoutExt = fileParts.slice(0, -1).join(".");
95 | return `${fileNameWithoutExt.substring(0, length)}.${extension}`;
96 | } catch (e) {
97 | return file;
98 | }
99 | };
100 |
101 | export const getFileSize = (size: number) => {
102 | size = Number(size);
103 | let unit = "KB";
104 | size = Math.ceil(size / 1000);
105 | if (size > 1000) {
106 | unit = "MB";
107 | size /= 1000;
108 | return `${size.toFixed(1)} ${unit}`;
109 | }
110 | return `${size} ${unit}`;
111 | };
112 |
113 | export const showSuccessMessage = (description: string) => {
114 | message.success(description);
115 | };
116 |
--------------------------------------------------------------------------------
/ui/src/services/endpoints.ts:
--------------------------------------------------------------------------------
1 | export const LOGIN = "/api/v1/user/login";
2 | export const IS_LOGGED_IN = "/api/v1/user/isLoggedIn";
3 | export const LIST_FOLDER = "/api/v1/folder/ls";
4 | export const MAKE_FOLDER = "/api/v1/folder/mkdir";
5 | export const DELETE_FOLDER = "/api/v1/folder/rmdir";
6 | export const DOWNLOAD_FILE_URL = "/api/v1/file/download";
7 | export const DELETE_FILE = "/api/v1/file/remove";
8 | export const UPLOAD_FILE = "/api/v1/file/upload";
9 | export const REGISTER = "/api/v1/user/register";
10 |
--------------------------------------------------------------------------------
/ui/src/services/file.ts:
--------------------------------------------------------------------------------
1 | import { makeRequest } from "./common";
2 | import { DELETE_FILE, DOWNLOAD_FILE_URL } from "./endpoints";
3 |
4 | export const getDownloadURL = async (fileId: string) => {
5 | try {
6 | const request = await makeRequest(DOWNLOAD_FILE_URL, "GET", {
7 | queryParams: {
8 | fileId,
9 | },
10 | });
11 | return request.data;
12 | } catch (e: any) {
13 | throw e;
14 | }
15 | };
16 |
17 | export const deleteFile = async (
18 | path: string,
19 | fileId: string,
20 | messageId: string
21 | ) => {
22 | try {
23 | const request = await makeRequest(DELETE_FILE, "POST", {
24 | body: {
25 | path,
26 | fileId,
27 | messageId,
28 | },
29 | });
30 | return request.data;
31 | } catch (e: any) {
32 | throw e;
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/ui/src/services/folder.ts:
--------------------------------------------------------------------------------
1 | import { makeRequest, showErrorMessage } from "./common";
2 | import { DELETE_FOLDER, LIST_FOLDER, MAKE_FOLDER } from "./endpoints";
3 |
4 | export const listFolder = async (path: string) => {
5 | const request = await makeRequest(LIST_FOLDER, "POST", {
6 | body: {
7 | path,
8 | },
9 | });
10 | return request.data;
11 | };
12 |
13 | export const makeFolder = async (path: string, folderName: string) => {
14 | try {
15 | const request = await makeRequest(MAKE_FOLDER, "POST", {
16 | body: {
17 | path,
18 | folderName,
19 | },
20 | });
21 | return request.data;
22 | } catch (e: any) {
23 | showErrorMessage("Make Folder Error", e?.response?.data?.message);
24 | throw e;
25 | }
26 | };
27 |
28 | export const deleteFolder = async (path: string) => {
29 | try {
30 | const request = await makeRequest(DELETE_FOLDER, "POST", {
31 | body: {
32 | path,
33 | },
34 | });
35 | return request.data;
36 | } catch (e: any) {
37 | showErrorMessage("Delete Folder Error", e?.response?.data?.message);
38 | throw e;
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/ui/src/services/user.ts:
--------------------------------------------------------------------------------
1 | import { history, makeRequest, showErrorMessage, showSuccessMessage } from "./common";
2 | import { IS_LOGGED_IN, LOGIN, REGISTER } from "./endpoints";
3 |
4 | export const handleLogin = async (values: any) => {
5 | try {
6 | const loginRequest = await makeRequest(LOGIN, "POST", {
7 | body: {
8 | username: values.username,
9 | password: values.password,
10 | },
11 | });
12 | const { data } = loginRequest;
13 | localStorage.setItem("accessToken", data.token);
14 | history.push("/home?path=%2Froot");
15 | } catch (e) {
16 | showErrorMessage("Authentication Error", "Invalid username or password");
17 | }
18 | };
19 |
20 | export const isLoggedIn = async () => {
21 | try {
22 | const loginStatus = await makeRequest(IS_LOGGED_IN, "GET");
23 | return loginStatus;
24 | } catch (e) {
25 | console.log("Unauthorized user!");
26 | return false;
27 | }
28 | };
29 |
30 | export const handleRegistration = async (values: any) => {
31 | try {
32 | const registrationResponse = await makeRequest(REGISTER, "POST", {
33 | body: {
34 | username: values.username,
35 | password: values.password,
36 | botToken: values.botToken,
37 | chatId : values.chatId
38 | },
39 | });
40 | const { data } = registrationResponse;
41 | showSuccessMessage("User : "+ data.username+" registered successfully.. Continue Login");
42 | history.push("/login");
43 | } catch (e) {
44 | showErrorMessage("Authentication Error", "Couldn't register"+values.username);
45 | }
46 | };
47 |
48 |
--------------------------------------------------------------------------------
/ui/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------