From 0f5d52795b617b46620bc80b519ae7cc1db1f440 Mon Sep 17 00:00:00 2001 From: Artem Golub Date: Thu, 7 Jul 2022 20:58:58 +0300 Subject: [PATCH] task: expose projects, folder restructure, minor fixes --- README.md | 4 + services/backend/src/main/settings.py | 4 +- services/frontend/src/App.tsx | 23 ++ .../src/components/Auth/Login/index.tsx | 13 +- .../src/components/Auth/Signup/index.tsx | 14 +- .../Modal/{Service => }/ConfirmDelete.tsx | 0 .../frontend/src/components/Profile/index.tsx | 37 +-- .../frontend/src/components/Project/index.tsx | 259 +++++++++--------- .../src/components/Projects/PreviewBlock.tsx | 91 ++++++ .../src/components/Projects/index.tsx | 65 +++++ services/frontend/src/components/SideBar.tsx | 132 --------- .../{ => global}/DarkModeSwitch/index.tsx | 0 .../DarkModeSwitch/userDarkMode.tsx | 0 .../src/components/global/Pagination.tsx | 32 +++ .../frontend/src/components/global/Search.tsx | 35 +++ .../src/components/global/SideBar.tsx | 62 +++++ .../src/components/{ => global}/Spinner.tsx | 0 .../src/components/{ => global}/UserMenu.tsx | 17 +- .../frontend/src/components/global/logo.tsx | 28 ++ services/frontend/src/components/logo.tsx | 20 -- .../frontend/src/components/useJsPlumb.ts | 16 +- services/frontend/src/constants/index.ts | 2 +- services/frontend/src/hooks/useProject.ts | 132 ++++++++- services/frontend/src/hooks/useProjects.ts | 30 ++ services/frontend/src/services/project.ts | 62 ----- services/frontend/src/types/index.ts | 9 + services/frontend/src/utils/index.ts | 11 + 27 files changed, 672 insertions(+), 426 deletions(-) rename services/frontend/src/components/Modal/{Service => }/ConfirmDelete.tsx (100%) create mode 100644 services/frontend/src/components/Projects/PreviewBlock.tsx create mode 100644 services/frontend/src/components/Projects/index.tsx delete mode 100644 services/frontend/src/components/SideBar.tsx rename services/frontend/src/components/{ => global}/DarkModeSwitch/index.tsx (100%) rename services/frontend/src/components/{ => global}/DarkModeSwitch/userDarkMode.tsx (100%) create mode 100644 services/frontend/src/components/global/Pagination.tsx create mode 100644 services/frontend/src/components/global/Search.tsx create mode 100644 services/frontend/src/components/global/SideBar.tsx rename services/frontend/src/components/{ => global}/Spinner.tsx (100%) rename services/frontend/src/components/{ => global}/UserMenu.tsx (63%) create mode 100644 services/frontend/src/components/global/logo.tsx delete mode 100644 services/frontend/src/components/logo.tsx create mode 100644 services/frontend/src/hooks/useProjects.ts delete mode 100644 services/frontend/src/services/project.ts diff --git a/README.md b/README.md index 4750022..e86c749 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,7 @@ $ cd services/frontend && npm run start - Ongoing improvements and features for docker compose yaml generation. - Kubernetes manifest generation. - Deployment to user's ECS, K8S, GS accounts. + +## Docs + +- https://docs.jsplumbtoolkit.com/community/ diff --git a/services/backend/src/main/settings.py b/services/backend/src/main/settings.py index c88cd56..58fbb8d 100644 --- a/services/backend/src/main/settings.py +++ b/services/backend/src/main/settings.py @@ -162,8 +162,9 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", "axes.backends.AxesBackend", - "django.contrib.auth.backends.ModelBackend", ] REST_FRAMEWORK = { @@ -186,6 +187,7 @@ if DEBUG: # allauth ACCOUNT_EMAIL_VERIFICATION = "none" ACCOUNT_PRESERVE_USERNAME_CASING = False +ACCOUNT_AUTHENTICATION_METHOD = "username_email" # dj_rest_auth REST_USE_JWT = True diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 6b00da6..9bd4ea2 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -11,6 +11,8 @@ import { checkHttpStatus } from "./services/helpers"; import { authSelf } from "./reducers"; import { refresh, self } from "./services/auth"; +import SideBar from "./components/global/SideBar"; +import Projects from "./components/Projects" import Project from "./components/Project"; import Profile from "./components/Profile"; import Signup from "./components/Auth/Signup"; @@ -80,6 +82,7 @@ export default function App() {
+ } /> + } + /> + } + /> + + } + /> + } + /> + { <>
-
-
-
- -
-
-
-

Sign in @@ -136,7 +127,7 @@ const Login = (props: IProfileProps) => { -
- - - - -
- -
-
-

- +

Profile

diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/index.tsx index eeb4d66..a28c5ef 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/index.tsx @@ -1,13 +1,13 @@ import { useEffect, useState, useRef, useMemo } from "react"; -import { useParams, useNavigate } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { debounce, Dictionary, omit } from 'lodash'; import YAML from "yaml"; import { PlusIcon } from "@heroicons/react/solid"; -import { IProjectPayload, IClientNodeItem, IServiceNodePosition } from "../../types"; +import { IProjectPayload, IClientNodeItem, IServiceNodePosition, IProject } from "../../types"; import eventBus from "../../events/eventBus"; -import { useProject, useUpdateProject } from "../../hooks/useProject"; +import { useMutation } from "react-query"; +import { useProject, useUpdateProject, createProject } from "../../hooks/useProject"; import useWindowDimensions from "../../hooks/useWindowDimensions"; -import { projectHttpCreate } from "../../services/project"; import { flattenGraphData } from "../../utils/generators"; import { nodeLibraries } from "../../utils/data/libraries"; import { @@ -20,20 +20,16 @@ import { import { checkHttpStatus } from "../../services/helpers"; import { generateHttp } from "../../services/generate"; import { Canvas } from "../Canvas"; -import Spinner from "../Spinner"; -import ModalConfirmDelete from "../Modal/Service/ConfirmDelete"; +import Spinner from "../global/Spinner"; +import ModalConfirmDelete from "../Modal/ConfirmDelete"; import ModalServiceCreate from "../Modal/Service/Create"; import ModalServiceEdit from "../Modal/Service/Edit"; import CodeEditor from "../CodeEditor"; -interface IProjectProps {} - -export default function Project(props: IProjectProps) { - const navigate = useNavigate(); +export default function Project() { const { uuid } = useParams<{ uuid: string }>(); const { height } = useWindowDimensions(); const { data, error, isFetching } = useProject(uuid); - const mutation = useUpdateProject(uuid); const stateNodesRef = useRef>(); const stateConnectionsRef = useRef<[[string, string]] | []>(); @@ -50,6 +46,15 @@ export default function Project(props: IProjectProps) { const [connections, setConnections] = useState<[[string, string]] | []>([]); const [projectName, setProjectName] = useState("Untitled"); const [canvasPosition, setCanvasPosition] = useState({top: 0, left: 0, scale: 1}); + const updateProjectMutation = useUpdateProject(uuid); + const createProjectMutation = useMutation((payload: IProjectPayload) => { + return createProject(payload) + }, + { + onSuccess: (project: IProject) => { + window.location.replace(`/projects/${project.uuid}`) + } + }); stateNodesRef.current = nodes; stateConnectionsRef.current = connections; @@ -58,17 +63,6 @@ export default function Project(props: IProjectProps) { setProjectName(e.target.value); } - const createProject = (payload: IProjectPayload) => { - projectHttpCreate(JSON.stringify(payload)) - .then(checkHttpStatus) - .then(data => { - navigate(`/projects/${data.uuid}`); - }) - .catch(err => {}) - .finally(() => { - }) - } - const onNodeUpdate = (positionData: IServiceNodePosition) => { if (stateNodesRef.current) { const node = { ...stateNodesRef.current[positionData.key], ...positionData }; @@ -95,9 +89,9 @@ export default function Project(props: IProjectProps) { } if (uuid) { - mutation.mutate(payload); + updateProjectMutation.mutate(payload); } else { - createProject(payload); + createProjectMutation.mutate(payload); } } @@ -120,8 +114,6 @@ export default function Project(props: IProjectProps) { return; } - console.log(data); - const canvasData = JSON.parse(data.data); const nodesAsList = Object.keys(canvasData.canvas.nodes).map(k => canvasData.canvas.nodes[k]); const clientNodeItems = getClientNodesAndConnections(nodesAsList, nodeLibraries); @@ -241,10 +233,6 @@ export default function Project(props: IProjectProps) { } }, [language, generatedCode]); - useEffect(() => { - - }, [nodeForEdit]); - if (!isFetching) { return ( <> @@ -276,120 +264,123 @@ export default function Project(props: IProjectProps) { : null } -
-
- +
+ - -
- - - -
- -
+ > + + +
+ + + +
+ +
-
-
-
-
-
- - - +
+
+
+
+
+ + + +
+ + onNodeUpdate(node)} + onGraphUpdate={(graphData: any) => onGraphUpdate(graphData)} + onCanvasUpdate={(canvasData: any) => onCanvasUpdate(canvasData)} + onConnectionAttached={(connectionData: any) => onConnectionAttached(connectionData)} + onConnectionDetached={(connectionData: any) => onConnectionDetached(connectionData)} + setNodeForEdit={(node: IClientNodeItem) => setNodeForEdit(node)} + setNodeForDelete={(node: IClientNodeItem) => setNodeForDelete(node)} + /> +
+
+
+
+ + +
- onNodeUpdate(node)} - onGraphUpdate={(graphData: any) => onGraphUpdate(graphData)} - onCanvasUpdate={(canvasData: any) => onCanvasUpdate(canvasData)} - onConnectionAttached={(connectionData: any) => onConnectionAttached(connectionData)} - onConnectionDetached={(connectionData: any) => onConnectionDetached(connectionData)} - setNodeForEdit={(node: IClientNodeItem) => setNodeForEdit(node)} - setNodeForDelete={(node: IClientNodeItem) => setNodeForDelete(node)} + { onCodeUpdate(e) }} + disabled={false} + lineWrapping={false} + height={height - 64} />
-
-
- - - -
- - { onCodeUpdate(e) }} - disabled={false} - lineWrapping={false} - height={height - 64} - /> -
- ) + ); } return ( diff --git a/services/frontend/src/components/Projects/PreviewBlock.tsx b/services/frontend/src/components/Projects/PreviewBlock.tsx new file mode 100644 index 0000000..ff33586 --- /dev/null +++ b/services/frontend/src/components/Projects/PreviewBlock.tsx @@ -0,0 +1,91 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { PencilIcon, TrashIcon } from "@heroicons/react/outline"; +import { truncateStr } from "../../utils"; +import { IProject } from "../../types"; +import ModalConfirmDelete from "../../components/Modal/ConfirmDelete"; +import { useDeleteProject } from "../../hooks/useProject"; + +interface IPreviewBlockProps { + project: IProject; +} + +const PreviewBlock = (props: IPreviewBlockProps) => { + const { project } = props; + const [isHovering, setIsHovering] = useState(false); + const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); + const objId = project.id; + const mutation = useDeleteProject(project.uuid); + + const handleMouseOver = () => { + setIsHovering(true); + }; + + const handleMouseLeave = () => { + setIsHovering(false); + }; + + const onDelete = () => { + setShowDeleteConfirmModal(true); + }; + + const onDeleteConfirmed = () => { + mutation.mutate(); + }; + + return ( + <> +
+
+ {truncateStr(project.name, 25)} +
+ + {isHovering && +
+ + + + + +
+ } +
+ + {showDeleteConfirmModal && + onDeleteConfirmed()} + onHide={() => { + setShowDeleteConfirmModal(false); + }} + /> + } + + ) +} + +export default PreviewBlock; diff --git a/services/frontend/src/components/Projects/index.tsx b/services/frontend/src/components/Projects/index.tsx new file mode 100644 index 0000000..fa249c7 --- /dev/null +++ b/services/frontend/src/components/Projects/index.tsx @@ -0,0 +1,65 @@ +import { Link } from "react-router-dom"; +import { IProject } from "../../types"; +import Spinner from "../../components/global/Spinner"; +import PreviewBlock from "./PreviewBlock"; +import { useProjects } from "../../hooks/useProjects"; + +const Projects = () => { + const { data, error, isFetching } = useProjects(); + + return ( + <> +
+
+
+
+

Projects

+ + + New + +
+ +
+ {isFetching && +
+ +
+ } + + {!isFetching && +
+
+ {(data.results.length > 0) && + data.results.map((project: IProject) => { + return ( +
+ +
+ ) + }) + } +
+ + {(data.results.length === 0) && +
+

Nothing here

+

Get started by creating a project.

+
+ } +
+ } +
+
+
+
+ + ) +} + +export default Projects; \ No newline at end of file diff --git a/services/frontend/src/components/SideBar.tsx b/services/frontend/src/components/SideBar.tsx deleted file mode 100644 index e1adf68..0000000 --- a/services/frontend/src/components/SideBar.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Fragment } from "react"; -import { useLocation } from "react-router-dom"; -import { Dialog, Transition } from "@headlessui/react"; -import { DatabaseIcon, TemplateIcon, XIcon } from "@heroicons/react/outline"; -import UserMenu from "./UserMenu"; -import Logo from "./logo"; - -interface ISideBarProps { - state: any; - sidebarOpen: boolean; - setSidebarOpen: any; -} - -export default function SideBar(props: ISideBarProps) { - const { pathname } = useLocation(); - const { state, sidebarOpen, setSidebarOpen } = props; - const navigation = [ - { name: "Templates", href: "/", icon: TemplateIcon, current: ((pathname === "/" || pathname.includes("templates")) ? true : false) }, - { name: "Connectors", href: "/connectors", icon: DatabaseIcon, current: (pathname.includes("connectors") ? true : false) } - ]; - const classNames = (...classes: any[]) => { - return classes.filter(Boolean).join(" ") - }; - const userName = state.user ? state.user.username : ""; - - return ( - <> - - - - - - -
- -
- -
-
- -
- -
- -
- -
- - -
-
- - -
-
- -
-
-
- -
-
- -
- - -
-
- - ); -} diff --git a/services/frontend/src/components/DarkModeSwitch/index.tsx b/services/frontend/src/components/global/DarkModeSwitch/index.tsx similarity index 100% rename from services/frontend/src/components/DarkModeSwitch/index.tsx rename to services/frontend/src/components/global/DarkModeSwitch/index.tsx diff --git a/services/frontend/src/components/DarkModeSwitch/userDarkMode.tsx b/services/frontend/src/components/global/DarkModeSwitch/userDarkMode.tsx similarity index 100% rename from services/frontend/src/components/DarkModeSwitch/userDarkMode.tsx rename to services/frontend/src/components/global/DarkModeSwitch/userDarkMode.tsx diff --git a/services/frontend/src/components/global/Pagination.tsx b/services/frontend/src/components/global/Pagination.tsx new file mode 100644 index 0000000..5c33f43 --- /dev/null +++ b/services/frontend/src/components/global/Pagination.tsx @@ -0,0 +1,32 @@ + +import Spinner from "./Spinner"; + + +interface IPaginationProps { + defaultCurrent: number; + defaultPageSize: number; + onChange: any; + total: number; + loading: boolean; +} + +const Pagination = (props: IPaginationProps) => { + const { defaultCurrent, onChange, total, loading } = props; + + return ( +
+ {`showing ${defaultCurrent} of ${total}`} + +
+ ) +}; + +export default Pagination; \ No newline at end of file diff --git a/services/frontend/src/components/global/Search.tsx b/services/frontend/src/components/global/Search.tsx new file mode 100644 index 0000000..05389a4 --- /dev/null +++ b/services/frontend/src/components/global/Search.tsx @@ -0,0 +1,35 @@ +import { SearchIcon } from "@heroicons/react/solid"; + +interface ISearchProps { + onSearchChange: any; +} + +const Search = (props: ISearchProps) => { + const { onSearchChange } = props; + + return ( +
+
+ + +
+
+
+ +
+
+
+ ) +} + +export default Search; diff --git a/services/frontend/src/components/global/SideBar.tsx b/services/frontend/src/components/global/SideBar.tsx new file mode 100644 index 0000000..c497d3b --- /dev/null +++ b/services/frontend/src/components/global/SideBar.tsx @@ -0,0 +1,62 @@ +import { useLocation } from "react-router-dom"; +import { BookOpenIcon } from "@heroicons/react/outline"; +import { Link } from "react-router-dom"; +import UserMenu from "./UserMenu"; +import Logo from "./logo"; + +interface ISideBarProps { + state: any; + isAuthenticated: boolean; +} + +export default function SideBar(props: ISideBarProps) { + const { pathname } = useLocation(); + const { state, isAuthenticated } = props; + const projRegex = /\/projects\/?$/; + const navigation = [{ + name: "Projects", + href: "/projects", + icon: BookOpenIcon, + current: (pathname.match(projRegex) ? true : false) + }]; + const classNames = (...classes: any[]) => { + return classes.filter(Boolean).join(" ") + }; + const userName = state.user ? state.user.username : ""; + + return ( + <> +
+
+
+ + + +
+ +
+ +
+ + +
+
+ + ); +} diff --git a/services/frontend/src/components/Spinner.tsx b/services/frontend/src/components/global/Spinner.tsx similarity index 100% rename from services/frontend/src/components/Spinner.tsx rename to services/frontend/src/components/global/Spinner.tsx diff --git a/services/frontend/src/components/UserMenu.tsx b/services/frontend/src/components/global/UserMenu.tsx similarity index 63% rename from services/frontend/src/components/UserMenu.tsx rename to services/frontend/src/components/global/UserMenu.tsx index f278495..ace5865 100644 --- a/services/frontend/src/components/UserMenu.tsx +++ b/services/frontend/src/components/global/UserMenu.tsx @@ -20,13 +20,16 @@ export default function UserMenu(props: IUserMenuProps) { flex border-t border-blue-800 p-4 w-full hover:cursor-pointer hover:bg-blue-600 `} > -
-
- -
-
-

{username}

-

View profile

+
+ +
+

+ {username + ? <>{username} + : <>Log in + } +

+

View profile

diff --git a/services/frontend/src/components/global/logo.tsx b/services/frontend/src/components/global/logo.tsx new file mode 100644 index 0000000..225e33b --- /dev/null +++ b/services/frontend/src/components/global/logo.tsx @@ -0,0 +1,28 @@ +interface ILogoProps { + className: string; +} + +const Logo = (props: ILogoProps) => { + const { className } = props; + return ( + + Nuxx + + + + + ) +} + +export default Logo; diff --git a/services/frontend/src/components/logo.tsx b/services/frontend/src/components/logo.tsx deleted file mode 100644 index 9194a56..0000000 --- a/services/frontend/src/components/logo.tsx +++ /dev/null @@ -1,20 +0,0 @@ -interface ILogoProps { - className: string; -} - -const Logo = (props: ILogoProps) => { - const { className } = props; - return ( - - - - - - - - - - ) -} - -export default Logo; \ No newline at end of file diff --git a/services/frontend/src/components/useJsPlumb.ts b/services/frontend/src/components/useJsPlumb.ts index bd15b7c..055a714 100644 --- a/services/frontend/src/components/useJsPlumb.ts +++ b/services/frontend/src/components/useJsPlumb.ts @@ -66,7 +66,7 @@ export const useJsPlumb = ( // arrow overlay for connector to specify // it's dependency on another service instance.addEndpoint(el, endpoint, { - anchor: [[0.4, 0, 0, -1], [1, 0.4, 1, 0], [0.4, 1, 0, 1], [0, 0.4, -1, 0]], + anchor: [[1, 0.6, 1, 0], [0, 0.6, -1, 0], [0.6, 1, 0, 1], [0.6, 0, 0, -1]], uuid: x.id, connectorOverlays: [{ type: "PlainArrow", @@ -85,7 +85,7 @@ export const useJsPlumb = ( endpoint.maxConnections = maxConnections; instance.addEndpoint(el, endpoint, { - anchor: [[0.6, 0, 0, -1], [1, 0.6, 1, 0], [0.6, 1, 0, 1], [0, 0.6, -1, 0]], + anchor: [[0, 0.4, -1, 0], [0.4, 1, 0, 1], [1, 0.4, 1, 0], [0.4, 0, 0, -1]], uuid: x.id }); }); @@ -185,8 +185,14 @@ export const useJsPlumb = ( onConnectionDetached([params.sourceId, firstConnection.suspendedElementId]); if (params.targetId !== firstConnection.suspendedElementId) { - onConnectionAttached([params.sourceId, params.targetId]); - return true; + const loopCheck = instance.select({ source: params.targetId as any, target: params.sourceId as any }); + + if (loopCheck.length > 0) { + return false; + } else { + onConnectionAttached([params.sourceId, params.targetId]); + return true; + } } } @@ -244,7 +250,7 @@ export const useJsPlumb = ( 'connections': getConnections(instance.getConnections({}, true) as Connection[]) }); } - }, [instance, addEndpoints, stateRef.current]); + }, [instance, addEndpoints, onGraphUpdate, stateRef.current]); useEffect(() => { if (!instance) return; diff --git a/services/frontend/src/constants/index.ts b/services/frontend/src/constants/index.ts index 2bd4b33..5b8abf4 100644 --- a/services/frontend/src/constants/index.ts +++ b/services/frontend/src/constants/index.ts @@ -1,3 +1,3 @@ export const API_SERVER_URL = process.env.REACT_APP_API_SERVER; export const PROJECTS_FETCH_LIMIT = 300; -export const LOCAL_STORAGE = 'CtkLocalStorage'; +export const LOCAL_STORAGE = 'NuxxLocalStorage'; diff --git a/services/frontend/src/hooks/useProject.ts b/services/frontend/src/hooks/useProject.ts index ff9d212..8961a58 100644 --- a/services/frontend/src/hooks/useProject.ts +++ b/services/frontend/src/hooks/useProject.ts @@ -1,22 +1,100 @@ -import axios from "axios" -import { useQuery, useMutation, useQueryClient, QueryClient } from "react-query"; +import axios from "axios"; +import _ from "lodash"; +import { useQuery, useMutation, useQueryClient } from "react-query"; import { API_SERVER_URL } from "../constants"; -import { IProjectPayload } from "../types"; +import { getLocalStorageJWTKeys } from "../utils"; +import { IProject, IProjectPayload } from "../types"; + +interface IProjectsReturn { + count: number; + next: string | null; + previous: string | null; + results: IProject[]; +} const fetchProjectByUuid = async (uuid: string) => { - const response = await axios.get(`${API_SERVER_URL}/projects/${uuid}/`); + const jwtKeys = getLocalStorageJWTKeys(); + const requestConfig = { + method: 'get', + url: `${API_SERVER_URL}/projects/${uuid}/`, + headers: { + "Content-Type": "application/json" + } + }; + + if (jwtKeys) { + requestConfig.headers = { + ...requestConfig.headers, + ...{"Authorization": `Bearer ${jwtKeys.access_token}`} + } + } + + const response = await axios(requestConfig); + return response.data; +} + +export const createProject = async (project: IProjectPayload) => { + const jwtKeys = getLocalStorageJWTKeys(); + const requestConfig = { + method: 'post', + url: `${API_SERVER_URL}/projects/`, + headers: { + "Content-Type": "application/json" + }, + data: project + }; + + if (jwtKeys) { + requestConfig.headers = { + ...requestConfig.headers, + ...{ "Authorization": `Bearer ${jwtKeys.access_token}` } + } + } + + const response = await axios(requestConfig); + return response.data; +} + +const deleteProjectByUuid = async (uuid: string) => { + const jwtKeys = getLocalStorageJWTKeys(); + const requestConfig = { + method: 'delete', + url: `${API_SERVER_URL}/projects/${uuid}/`, + headers: { + "Content-Type": "application/json" + } + }; + + if (jwtKeys) { + requestConfig.headers = { + ...requestConfig.headers, + ...{ "Authorization": `Bearer ${jwtKeys.access_token}` } + } + } + + const response = await axios(requestConfig); return response.data; } const updateProjectByUuid = async (uuid: string, data: string) => { - const response = await axios({ + const jwtKeys = getLocalStorageJWTKeys(); + const requestConfig = { method: 'put', url: `${API_SERVER_URL}/projects/${uuid}/`, headers: { "Content-Type": "application/json" }, data: data - }); + }; + + if (jwtKeys) { + requestConfig.headers = { + ...requestConfig.headers, + ...{ "Authorization": `Bearer ${jwtKeys.access_token}` } + } + } + + const response = await axios(requestConfig); return response.data; } @@ -49,9 +127,9 @@ export const useUpdateProject = (uuid: string | undefined) => { return data; } catch (err: any) { if (err.response.status === 404) { - console.log('Resource could not be found!'); + console.error('Resource could not be found!'); } else { - console.log(err.message); + console.error(err.message); } } }, @@ -62,3 +140,41 @@ export const useUpdateProject = (uuid: string | undefined) => { } ) } + +export const useDeleteProject = (uuid: string | undefined) => { + const queryClient = useQueryClient(); + + return useMutation( + async () => { + if (!uuid) { + return; + } + + try { + const data = await deleteProjectByUuid(uuid); + return data; + } catch (err: any) { + if (err.response.status === 404) { + console.error('Resource could not be found!'); + } else { + console.error(err.message); + } + } + }, + { + onSuccess: () => { + // could just invalidate the query here and refetch everything + // queryClient.invalidateQueries(['projects']); + + queryClient.cancelQueries('projects'); + const previousProjects = queryClient.getQueryData('projects') as IProjectsReturn; + const filtered = _.filter(previousProjects.results, (project, index) => { + return project.uuid !== uuid + }); + previousProjects.count = filtered.length; + previousProjects.results = filtered; + queryClient.setQueryData('projects', previousProjects); + } + } + ) +} diff --git a/services/frontend/src/hooks/useProjects.ts b/services/frontend/src/hooks/useProjects.ts new file mode 100644 index 0000000..5db2769 --- /dev/null +++ b/services/frontend/src/hooks/useProjects.ts @@ -0,0 +1,30 @@ +import axios from "axios" +import { useQuery } from "react-query"; +import { API_SERVER_URL, PROJECTS_FETCH_LIMIT } from "../constants"; +import { getLocalStorageJWTKeys } from "../utils"; + +const fetchProjects = async () => { + const jwtKeys = getLocalStorageJWTKeys(); + + const response = await axios({ + method: 'get', + url: `${API_SERVER_URL}/projects/`, + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${jwtKeys.access_token}` + } + }); + return response.data; +} + +export const useProjects = () => { + return useQuery( + ["projects"], + async () => { + return await fetchProjects(); + }, + { + staleTime: Infinity + } + ) +} diff --git a/services/frontend/src/services/project.ts b/services/frontend/src/services/project.ts deleted file mode 100644 index bdc027f..0000000 --- a/services/frontend/src/services/project.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { IProjectPayload } from "../types"; -import { API_SERVER_URL, PROJECTS_FETCH_LIMIT } from "../constants"; -import { getLocalStorageJWTKeys } from "./utils"; - -export const projectHttpCreate = (data: string) => { - //const jwtKeys = getLocalStorageJWTKeys(); - return fetch(`${API_SERVER_URL}/projects/`, { - method: "POST", - headers: { - "Content-Type": "application/json", - //"Authorization": `Bearer ${jwtKeys.access_token}` - }, - body: data - }); -} - -export const projectHttpUpdate = (uuid: string, data: string) => { - //const jwtKeys = getLocalStorageJWTKeys(); - return fetch(`${API_SERVER_URL}/projects/${uuid}/`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - //"Authorization": `Bearer ${jwtKeys.access_token}` - }, - body: data - }); -} - -export const projectHttpDelete = (uuid: number) => { - const jwtKeys = getLocalStorageJWTKeys(); - return fetch(`${API_SERVER_URL}/projects/${uuid}/`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${jwtKeys.access_token}` - } - }); -} - -export const projectsHttpGet = (offset: number) => { - const jwtKeys = getLocalStorageJWTKeys(); - let endpoint = `${API_SERVER_URL}/projects/?limit=${PROJECTS_FETCH_LIMIT}&offset=${offset}`; - - return fetch(endpoint, { - method: "GET", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${jwtKeys.access_token}` - } - }); -} - -export const projectHttpGet = (uuid: string) => { - //const jwtKeys = getLocalStorageJWTKeys(); - return fetch(`${API_SERVER_URL}/projects/${uuid}/`, { - method: "GET", - headers: { - "Content-Type": "application/json", - //"Authorization": `Bearer ${jwtKeys.access_token}` - } - }); -} diff --git a/services/frontend/src/types/index.ts b/services/frontend/src/types/index.ts index 90a03a1..5b6e34e 100644 --- a/services/frontend/src/types/index.ts +++ b/services/frontend/src/types/index.ts @@ -10,6 +10,15 @@ export interface IServiceNodePosition { } } +export interface IProject { + id: number; + name: string; + uuid: string; + data: string; + created_at: string; + modified_at: string; +} + export interface IContainer { name: string; args?: string[]; diff --git a/services/frontend/src/utils/index.ts b/services/frontend/src/utils/index.ts index 8dc9332..c6f8283 100644 --- a/services/frontend/src/utils/index.ts +++ b/services/frontend/src/utils/index.ts @@ -10,6 +10,7 @@ import { range, values } from "lodash"; +import { LOCAL_STORAGE } from "../constants"; import { IClientNodeItem, IServiceNodeItem, @@ -240,3 +241,13 @@ export const truncateStr = (str: string, length: number) => { export const getMatchingSetIndex = (setOfSets: [[string, string]], findSet: [string, string]): number => { return setOfSets.findIndex((set) => set.toString() === findSet.toString()); } + +export const getLocalStorageJWTKeys = () => { + let jwtKeys = localStorage.getItem(LOCAL_STORAGE); + + if (jwtKeys) { + return JSON.parse(jwtKeys); + } + + return null; +}