diff --git a/GUI/front-end/src/components/displays/extension/FileInstance.tsx b/GUI/front-end/src/components/displays/extension/FileInstance.tsx new file mode 100644 index 0000000..19986e7 --- /dev/null +++ b/GUI/front-end/src/components/displays/extension/FileInstance.tsx @@ -0,0 +1,132 @@ +import { Box, Typography, useTheme, IconButton, Button } from "@mui/material"; +import { useLanguageContext } from "../../../contexts"; +import { useEffect, useState } from "react"; +import { useDeleteFile, useGetWorkspaceFiles, useUploadFile } from "../../../hooks"; +import { useWorkspaceContext } from "../../../contexts/tool/UseWorkspaceContext"; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import FileInput from "../../fileInput/FileInput"; + +const FileInstance: React.FC = () => { + + const Theme = useTheme(); + + const languageContext = useLanguageContext(); + const workspaceContext = useWorkspaceContext(); + const deleteFile = useDeleteFile(); + const createFile = useUploadFile(); + + + const [files, setFiles] = useState([]); + const [file, setFile] = useState(null); + const [fileName, setFileName] = useState(''); + + const { data, isLoading, isFetching } = useGetWorkspaceFiles(workspaceContext.workspace as string); + + useEffect(() => { + if (data?.files.length > 0) { + setFiles(data.files); + } else { + setFiles([]); + } + }, [data]); + + const onFileSelect = (selectedFile: string | ArrayBuffer | null, name: string) => { + setFile(selectedFile); + setFileName(name); + } + + const createNewFile = async () => { + + const binaryData = dataURLtoBlob(file); + + const data = new FormData(); + data.append('workspace', workspaceContext.workspace as string); + data.append('file', binaryData); + data.append('file_name', fileName); + + await createFile.mutateAsync(data); + } + + const dataURLtoBlob = (file : string | ArrayBuffer | null) => { + const stringFile = file as string; + const arr = stringFile.split(","); + const mimeMatch = arr[0].match(/:(.*?);/); + + if (!mimeMatch) { + return new Blob(); + } + + const mime = mimeMatch[1]; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + return new Blob([u8arr], { type: mime }); + }; + + return ( + <> + {workspaceContext.workspace && !isLoading && !isFetching && ( + <> + {languageContext.language === 'en' ? 'Files' : 'Failai'} + + + + {file && fileName && (fileName)} + { + files?.length > 0 ? ( + files.map((file) => ( + + {file} + { + const data = { + workspace: workspaceContext.workspace, + file_name: file, + } + + await deleteFile.mutateAsync(data); + }} + > + + + + )) + ) : ( + {languageContext.language === 'en' ? 'No files' : 'Nėra failų'} + )} + + + )} + + ); +} + +export default FileInstance; \ No newline at end of file diff --git a/GUI/front-end/src/components/displays/extension/FunctionalityInstance.tsx b/GUI/front-end/src/components/displays/extension/FunctionalityInstance.tsx index 8464c1a..b8f15da 100644 --- a/GUI/front-end/src/components/displays/extension/FunctionalityInstance.tsx +++ b/GUI/front-end/src/components/displays/extension/FunctionalityInstance.tsx @@ -2,6 +2,7 @@ import { ArrowDropDown as ArrowDropDownIcon } from '@mui/icons-material/'; import { Box, MenuItem, Select, Typography, styled, useTheme } from '@mui/material'; import { useLanguageContext, useToolContext } from '../../../contexts'; import React from 'react'; +import WorkspaceInstance from './WorkspaceInstance'; interface Props {} @@ -44,6 +45,8 @@ const FunctionalityInstance: React.FC = () => { ))} + {languageContext.language === 'en' ? 'Workspace' : 'Darbo aplinka'} + ); }; diff --git a/GUI/front-end/src/components/displays/extension/WorkspaceInstance.tsx b/GUI/front-end/src/components/displays/extension/WorkspaceInstance.tsx new file mode 100644 index 0000000..7e821b2 --- /dev/null +++ b/GUI/front-end/src/components/displays/extension/WorkspaceInstance.tsx @@ -0,0 +1,115 @@ +import { Select, useTheme, styled, MenuItem, TextField, Button, IconButton } from "@mui/material"; +import { ArrowDropDown as ArrowDropDownIcon } from '@mui/icons-material/'; +import { useLanguageContext } from "../../../contexts"; +import { useWorkspaceContext } from "../../../contexts/tool/UseWorkspaceContext"; +import { useEffect, useState } from "react"; +import { useCreateWorkspace, useDeleteWorkspace, useGetWorkspaces } from "../../../hooks"; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import FileInstance from "./FileInstance"; + +const WorkspaceInstance: React.FC = () => { + const Theme = useTheme(); + + const languageContext = useLanguageContext(); + const workspaceContext = useWorkspaceContext(); + + const [awailableWorkspaces, setAwailableWorkspaces] = useState([]); + const [newWorkspace, setNewWorkspace] = useState(''); + + const { data, isLoading, isFetching } = useGetWorkspaces(); + const createWorkspace = useCreateWorkspace(); + const deleteWorkspace = useDeleteWorkspace(); + + const createNewWorkspace = () => { + createWorkspace.mutate(newWorkspace); + workspaceContext.update(newWorkspace); + setNewWorkspace(''); + } + + useEffect(() => { + if (data?.workspaces.length > 0) { + setAwailableWorkspaces(data.workspaces); + } else { + setAwailableWorkspaces([]); + } + }, [data]); + + return ( + <> + setNewWorkspace(e.target.value)} + /> + + {awailableWorkspaces.length > 0 && !isFetching && !isLoading && ( + <> + + + + )} + + ); +} + +export default WorkspaceInstance; \ No newline at end of file diff --git a/GUI/front-end/src/components/fileInput/FileInput.tsx b/GUI/front-end/src/components/fileInput/FileInput.tsx new file mode 100644 index 0000000..2bd40cc --- /dev/null +++ b/GUI/front-end/src/components/fileInput/FileInput.tsx @@ -0,0 +1,57 @@ +import { Button } from "@mui/material"; +import { useRef } from "react"; +import { useLanguageContext } from "../../contexts"; + +function FileInput({ onFileSelect } : { onFileSelect: (file: string | ArrayBuffer | null, name: string) => void}){ + const languageContext = useLanguageContext(); + const inputRef = useRef(null); + + const handleFileSelect = (e: React.ChangeEvent) => { + if(e.target.files && e.target.files.length > 0){ + const file = e.target.files[0]; + + if(!['text/plain'].includes(file.type)){ + alert('Please select a text file'); + return; + } + + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + onFileSelect(reader.result, file.name); + }; + + if (inputRef.current) { + inputRef.current.value = ''; + } + } + + + }; + + const onFileInputClick = () => { + if (inputRef.current) { + inputRef.current.click(); + } + }; + + return ( +
+ + +
+ ); +} + +export default FileInput; \ No newline at end of file diff --git a/GUI/front-end/src/contexts/index.ts b/GUI/front-end/src/contexts/index.ts index b32bde7..b87254d 100644 --- a/GUI/front-end/src/contexts/index.ts +++ b/GUI/front-end/src/contexts/index.ts @@ -6,3 +6,4 @@ export { ApplicationContextProvider } from './application/ApplicationContextProv export { useApplicationContext } from './application/UseApplicationContext'; export { ToolContextProvider } from './tool/ToolContextProvider'; export { useToolContext } from './tool/UseToolContext'; +export { WorkspaceContextProvider, WorkspaceContext } from './tool/WorkspaceContextProvider'; diff --git a/GUI/front-end/src/contexts/tool/UseWorkspaceContext.ts b/GUI/front-end/src/contexts/tool/UseWorkspaceContext.ts new file mode 100644 index 0000000..1d7047f --- /dev/null +++ b/GUI/front-end/src/contexts/tool/UseWorkspaceContext.ts @@ -0,0 +1,4 @@ +import { useContext } from "react"; +import { WorkspaceContext } from "./WorkspaceContextProvider"; + +export const useWorkspaceContext = () => useContext(WorkspaceContext); \ No newline at end of file diff --git a/GUI/front-end/src/contexts/tool/WorkspaceContextProvider.tsx b/GUI/front-end/src/contexts/tool/WorkspaceContextProvider.tsx new file mode 100644 index 0000000..1ca958d --- /dev/null +++ b/GUI/front-end/src/contexts/tool/WorkspaceContextProvider.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useState } from "react"; + +interface WorkspaceContextProps { + workspace: string | null; + update: (newWorkspace: string) => void; +} + +export const WorkspaceContext = createContext({ + workspace: null, + update: () => {}, +}) + +interface Props { + children?: React.ReactNode; +} + +export const WorkspaceContextProvider: React.FC = ({ children }) => { + const [workspace, setWorkspace] = useState(null); + + function updateWorkspace(newWorkspace: string) { + setWorkspace(newWorkspace); + } + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/GUI/front-end/src/hooks/index.ts b/GUI/front-end/src/hooks/index.ts index 1ceca83..69b5d48 100644 --- a/GUI/front-end/src/hooks/index.ts +++ b/GUI/front-end/src/hooks/index.ts @@ -1,3 +1,3 @@ export { useGetUsers } from './user/userHook'; export { useSendRequest } from './request/requestHook'; -export { useGetWorkspaces, useCreateWorkspace, useDeleteWorkspace, useGetWorkspaceFiles } from './workspace/workspaceHook'; +export { useGetWorkspaces, useCreateWorkspace, useDeleteWorkspace, useGetWorkspaceFiles, useUploadFile, useDeleteFile } from './workspace/workspaceHook'; diff --git a/GUI/front-end/src/hooks/workspace/workspaceHook.ts b/GUI/front-end/src/hooks/workspace/workspaceHook.ts index 6f8a0d2..f94b474 100644 --- a/GUI/front-end/src/hooks/workspace/workspaceHook.ts +++ b/GUI/front-end/src/hooks/workspace/workspaceHook.ts @@ -1,5 +1,5 @@ import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query"; -import { createWorkspace, getWorkspaces, getWorkspaceFiles, deleteWorkspace } from "../../services"; +import { createWorkspace, getWorkspaces, getWorkspaceFiles, deleteWorkspace, uploadFile, deleteFile } from "../../services"; export const useCreateWorkspace = () => { const queryClient = useQueryClient(); @@ -39,4 +39,26 @@ export const useDeleteWorkspace = () => { queryClient.invalidateQueries({ queryKey: ['workspace'] }); }, }); -}; \ No newline at end of file +}; + +export const useUploadFile = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: uploadFile, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['workspaceFiles'] }); + }, + }); +} + +export const useDeleteFile = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteFile, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['workspaceFiles'] }); + }, + }); +} \ No newline at end of file diff --git a/GUI/front-end/src/main/main.tsx b/GUI/front-end/src/main/main.tsx index cb2e7e8..414ea92 100644 --- a/GUI/front-end/src/main/main.tsx +++ b/GUI/front-end/src/main/main.tsx @@ -6,6 +6,7 @@ import { ThemeContextProvider, ApplicationContextProvider, ToolContextProvider, + WorkspaceContextProvider, } from '../contexts'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -15,13 +16,15 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - + + + + + + + + + diff --git a/GUI/front-end/src/services/index.ts b/GUI/front-end/src/services/index.ts index 6e85c87..a6818c5 100644 --- a/GUI/front-end/src/services/index.ts +++ b/GUI/front-end/src/services/index.ts @@ -1,3 +1,3 @@ export { getUsers } from './user/userService'; export { sendRequest } from './request/requestService'; -export { createWorkspace, getWorkspaces, getWorkspaceFiles, deleteWorkspace } from './workspace/workspaceService'; +export { createWorkspace, getWorkspaces, getWorkspaceFiles, deleteWorkspace, uploadFile, deleteFile } from './workspace/workspaceService'; diff --git a/GUI/front-end/src/services/workspace/workspaceService.ts b/GUI/front-end/src/services/workspace/workspaceService.ts index d01c470..4e23da0 100644 --- a/GUI/front-end/src/services/workspace/workspaceService.ts +++ b/GUI/front-end/src/services/workspace/workspaceService.ts @@ -21,6 +21,9 @@ export async function getWorkspaces() { export async function getWorkspaceFiles({ queryKey } : { queryKey: [string, string] }) { const [, workspace] = queryKey; + + if(workspace === null) return Promise.resolve({ files: [] }); + return await httpClient .get(ENDPOINTS.WORKSPACE.GET_WORKSPACE_FILES(workspace)) .then((res) => res.data) @@ -36,4 +39,27 @@ export async function deleteWorkspace(workspace: string) { .catch((err) => { console.error(err); }); +} + +export async function uploadFile(data: FormData) { + const originalContentType = httpClient.defaults.headers['Content-Type']; + httpClient.defaults.headers['Content-Type'] = 'multipart/form-data'; + + try { + const response = await httpClient.post(ENDPOINTS.WORKSPACE.UPLOAD_FILE, data); + return response.data; + } catch (error) { + console.error(error); + } finally { + httpClient.defaults.headers['Content-Type'] = originalContentType; + } +} + +export async function deleteFile(data: { workspace: string | null; file_name: string }) { + return await httpClient + .delete(ENDPOINTS.WORKSPACE.DELETE_FILE, { data }) + .then((res) => res.data) + .catch((err) => { + console.error(err); + }); } \ No newline at end of file diff --git a/GUI/front-end/src/types/constants/endpoints.ts b/GUI/front-end/src/types/constants/endpoints.ts index 79b5861..18e0d98 100644 --- a/GUI/front-end/src/types/constants/endpoints.ts +++ b/GUI/front-end/src/types/constants/endpoints.ts @@ -7,6 +7,8 @@ export const ENDPOINTS = { CREATE_WORKSPACE: '/workspace', GET_WORKSPACES: '/workspace', GET_WORKSPACE_FILES: (workspace : string) => `/workspace/${workspace}`, - DELETE_WORKSPACE: (workspace : string) => `/workspace/${workspace}` + DELETE_WORKSPACE: (workspace : string) => `/workspace/${workspace}`, + UPLOAD_FILE: `/workspace/file/upload`, + DELETE_FILE: `/workspace/file/delete`, } }; \ No newline at end of file