task: expose projects, folder restructure, minor fixes

remotes/origin/refactor/jwt-ls
Artem Golub 3 years ago
parent 74b90ae4ff
commit 59ea695935

@ -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/

@ -162,8 +162,9 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesBackend",
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
"axes.backends.AxesBackend",
]
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

@ -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() {
<QueryClientProvider client={queryClient}>
<div>
<Toaster />
<SideBar isAuthenticated={isAuthenticated} state={state} />
<Routes>
<Route
path="/projects/:uuid"
@ -91,6 +94,26 @@ export default function App() {
element={<Project />}
/>
<Route
path="/"
element={
<ProtectedRoute
{...defaultProtectedRouteProps}
outlet={<Projects />}
/>
}
/>
<Route
path="/projects/"
element={
<ProtectedRoute
{...defaultProtectedRouteProps}
outlet={<Projects />}
/>
}
/>
<Route
path="/profile"
element={

@ -1,8 +1,7 @@
import { useState } from "react";
import { useFormik } from "formik";
import { Link, useNavigate } from "react-router-dom";
import DarkModeSwitch from "../../../components/DarkModeSwitch";
import Spinner from "../../../components/Spinner";
import Spinner from "../../../components/global/Spinner";
import { toaster } from "../../../utils";
import { checkHttpStatus } from "../../../services/helpers";
import { logIn } from "../../../services/auth";
@ -55,14 +54,6 @@ const Login = (props: IProfileProps) => {
<>
<div className="flex flex-col">
<div className="dark:bg-gray-800 sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow">
<div className="flex-1 px-4 sm:px-6 md:px-8 flex justify-end items-center">
<div className="ml-4 flex md:ml-6">
<DarkModeSwitch />
</div>
</div>
</div>
<main className="py-6 md:w-2/3 lg:w-1/4 mx-auto">
<h2 className="mb-4 px-4 sm:px-6 md:flex-row md:px-8 text-xl font-extrabold dark:text-white text-gray-900">
Sign in
@ -136,7 +127,7 @@ const Login = (props: IProfileProps) => {
<button
onClick={() => formik.handleSubmit()}
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-3 py-1 bg-green-600 text-sm font-medium text-white hover:bg-green-700 sm:w-auto text-sm"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-2.5 py-1.5 bg-green-600 text-sm font-medium text-white hover:bg-green-700 sm:w-auto text-sm"
>
<div className="flex justify-center items-center space-x-2">
{loggingIn &&

@ -1,8 +1,7 @@
import { useState } from "react";
import { useFormik } from "formik";
import { Link, useNavigate } from "react-router-dom";
import DarkModeSwitch from "../../../components/DarkModeSwitch";
import Spinner from "../../../components/Spinner";
import Spinner from "../../../components/global/Spinner";
import { toaster } from "../../../utils";
import { checkHttpStatus } from "../../../services/helpers";
import { signup } from "../../../services/auth";
@ -58,16 +57,7 @@ const Signup = (props: IProfileProps) => {
return (
<>
<div className="flex flex-col">
<div className="dark:bg-gray-800 sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow">
<div className="flex-1 px-4 sm:px-6 md:px-8 flex justify-end items-center">
<div className="ml-4 flex md:ml-6">
<DarkModeSwitch />
</div>
</div>
</div>
<main className="py-6 md:w-2/3 lg:w-1/4 mx-auto">
<h2 className="mb-4 px-4 sm:px-6 md:flex-row md:px-8 text-xl font-extrabold dark:text-white text-gray-900">
Create account
@ -205,7 +195,7 @@ const Signup = (props: IProfileProps) => {
<button
onClick={() => formik.handleSubmit()}
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-3 py-1 bg-green-600 text-sm font-medium text-white hover:bg-green-700 sm:w-auto text-sm"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-2.5 py-1.5 bg-green-600 text-sm font-medium text-white hover:bg-green-700 sm:w-auto text-sm"
>
<div className="flex justify-center items-center space-x-2">
{signingUp &&

@ -1,8 +1,5 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { MenuAlt2Icon, ReplyIcon } from "@heroicons/react/outline";
import SideBar from "../../components/SideBar";
import DarkModeSwitch from "../../components/DarkModeSwitch";
import SideBar from "../../components/global/SideBar";
import { LOCAL_STORAGE } from "../../constants";
import { authSelf } from "../../reducers";
@ -13,7 +10,6 @@ interface IProfileProps {
const Profile = (props: IProfileProps) => {
const { dispatch, state } = props;
const [sidebarOpen, setSidebarOpen] = useState(false);
const logOut = () => {
localStorage.removeItem(LOCAL_STORAGE);
@ -22,40 +18,15 @@ const Profile = (props: IProfileProps) => {
return (
<>
<SideBar state={state} sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
<div className="md:pl-56 flex flex-col flex-1">
<div className="dark:bg-gray-800 sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow">
<button
type="button"
className="px-4 border-r dark:border-gray-900 border-gray-200 text-gray-500 md:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<MenuAlt2Icon className="h-6 w-6" aria-hidden="true" />
</button>
<div className="flex-1 px-4 sm:px-6 md:px-8 flex justify-between items-center">
<Link
className="text-gray-700 dark:text-white"
to="/"
>
<ReplyIcon className="w-4 h-4" />
</Link>
<div className="ml-4 flex md:ml-6">
<DarkModeSwitch />
</div>
</div>
</div>
<div className="md:pl-16 flex flex-col flex-1">
<main className="py-6">
<div className="flex justify-between px-4 sm:px-6 md:px-8">
<h1 className="text-2xl font-semibold dark:text-white text-gray-900">Profile</h1>
<button
className="flex items-center bg-blue-600 hover:bg-blue-700 text-white font-medium py-1 px-3 rounded focus:outline-none focus:shadow-outline"
className="btn-util text-white bg-blue-600 hover:bg-blue-700 sm:w-auto"
onClick={logOut}
>
<span className="text-sm">Logout</span>
<span>Logout</span>
</button>
</div>
<div className="grid grid-cols-1 gap-x-4 gap-y-8 px-4 py-4 sm:px-6 md:flex-row md:px-8">

@ -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<Dictionary<IClientNodeItem>>();
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,9 +264,10 @@ export default function Project(props: IProjectProps) {
: null
}
<div className="md:pl-16 flex flex-col flex-1">
<div className="px-4 py-3 border-b border-gray-200">
<form
className="flex flex-col space-y-2 md:flex-row md:justify-between items-center"
className="flex flex-col space-y-2 md:space-y-0 md:flex-row md:justify-between items-center"
autoComplete="off"
>
<input
@ -331,7 +320,8 @@ export default function Project(props: IProjectProps) {
className="btn-util text-white bg-green-600 hover:bg-green-700 sm:w-auto"
>
<div className="flex justify-center items-center space-x-2 mx-auto">
{mutation.isLoading && <Spinner className="w-4 h-4 text-green-300" />}
{updateProjectMutation.isLoading && <Spinner className="w-4 h-4 text-green-300" />}
{createProjectMutation.isLoading && <Spinner className="w-4 h-4 text-green-300" />}
<span>Save</span>
</div>
</button>
@ -340,7 +330,7 @@ export default function Project(props: IProjectProps) {
</div>
<div className="flex flex-grow relative flex-col md:flex-row">
<div className="w-full overflow-hidden md:w-2/3 z-40" style={{ height: height }}>
<div className="w-full overflow-hidden md:w-2/3 z-40" style={{ height: (height - 64) }}>
<div className="relative h-full">
<div className="absolute top-0 right-0 z-40">
<div className="flex space-x-2 p-2">
@ -388,8 +378,9 @@ export default function Project(props: IProjectProps) {
/>
</div>
</div>
</div>
</>
)
);
}
return (

@ -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 (
<>
<div
onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave}
key={project.id}
className={`
relative
rounded-lg
dark:bg-gray-900
bg-gray-100
px-6
py-5
shadow-sm
flex
items-center
space-x-3
hover:border-gray-400
`}
>
<div className="flex-1 min-w-0">
{truncateStr(project.name, 25)}
</div>
{isHovering &&
<div className="flex space-x-1 absolute top-2 right-2">
<button
onClick={() => onDelete()}
className="flex justify-center items-center p-2 hover:bg-gray-100 shadow bg-white rounded-md"
>
<TrashIcon className="w-3 h-3 text-red-500 hover:text-red-600" />
</button>
<Link
to={`/projects/${project.uuid}`}
className="flex justify-center items-center p-2 hover:bg-gray-100 shadow bg-white rounded-md"
>
<PencilIcon className="w-3 h-3 text-gray-500 hover:text-gray-600" />
</Link>
</div>
}
</div>
{showDeleteConfirmModal &&
<ModalConfirmDelete
onConfirm={() => onDeleteConfirmed()}
onHide={() => {
setShowDeleteConfirmModal(false);
}}
/>
}
</>
)
}
export default PreviewBlock;

@ -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 (
<>
<div className="md:pl-16 flex flex-col flex-1">
<main>
<div className="py-6">
<div className="flex justify-between px-4 sm:px-6 md:px-8">
<h1 className="text-2xl font-semibold dark:text-white text-gray-900">Projects</h1>
<Link
className="btn-util text-white bg-blue-600 hover:bg-blue-700 sm:w-auto"
to="/projects/new"
>
<span>New</span>
</Link>
</div>
<div className="px-4 sm:px-6 md:px-8">
{isFetching &&
<div className="flex justify-center items-center mx-auto mt-10">
<Spinner className="w-6 h-6 text-blue-600" />
</div>
}
{!isFetching &&
<div className="py-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{(data.results.length > 0) &&
data.results.map((project: IProject) => {
return (
<div key={`${project.uuid}`}>
<PreviewBlock
project={project}
/>
</div>
)
})
}
</div>
{(data.results.length === 0) &&
<div className="text-center">
<h3 className="mt-12 text-sm font-medium text-gray-900 dark:text-white">Nothing here</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">Get started by creating a project.</p>
</div>
}
</div>
}
</div>
</div>
</main>
</div>
</>
)
}
export default Projects;

@ -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 (
<>
<Transition.Root show={sidebarOpen} as={Fragment}>
<Dialog as="div" className="fixed inset-0 flex z-40 md:hidden" onClose={setSidebarOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-700 bg-opacity-50" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<div className="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-blue-700">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute top-0 right-0 -mr-7 pt-2">
<button
type="button"
className="ml-1 flex items-center justify-center h-5 w-5 rounded-full"
onClick={() => setSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</Transition.Child>
<div className="flex-shrink-0 flex items-center px-4">
<Logo className="w-5 h-5" />
</div>
<div className="mt-5 flex-1 h-0 overflow-y-auto">
<nav className="px-2 space-y-1">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className={classNames(
item.current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600",
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
)}
>
<item.icon className="mr-4 flex-shrink-0 h-6 w-6" aria-hidden="true" />
{item.name}
</a>
))}
</nav>
</div>
<UserMenu username={userName} current={pathname.includes("profile")} />
</div>
</Transition.Child>
<div className="flex-shrink-0 w-14" aria-hidden="true">
{/* Dummy element to force sidebar to shrink to fit close icon */}
</div>
</Dialog>
</Transition.Root>
<div className="hidden md:flex md:w-56 md:flex-col md:fixed md:inset-y-0">
<div className="flex flex-col flex-grow pt-5 bg-blue-700 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4">
<Logo className="w-5 h-5" />
</div>
<div className="mt-5 flex-1 flex flex-col">
<nav className="flex-1 px-2 pb-4 space-y-1">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className={classNames(
item.current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600",
"group flex items-center px-2 py-2 text-sm font-medium rounded-md"
)}
>
<item.icon className="mr-3 flex-shrink-0 h-5 w-5" aria-hidden="true" />
{item.name}
</a>
))}
</nav>
</div>
<UserMenu username={userName} current={pathname.includes("profile")} />
</div>
</div>
</>
);
}

@ -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 (
<div className="text-center">
<span className="block mb-5 text-sm">{`showing ${defaultCurrent} of ${total}`}</span>
<button
className="inline-flex items-center px-4 py-2 border border-transparent
text-xs font-medium rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700"
type="button"
disabled={loading}
onClick={() => onChange()}
>
{loading ? <Spinner className="w-4 h-4" /> : <span>load more</span>}
</button>
</div>
)
};
export default Pagination;

@ -0,0 +1,35 @@
import { SearchIcon } from "@heroicons/react/solid";
interface ISearchProps {
onSearchChange: any;
}
const Search = (props: ISearchProps) => {
const { onSearchChange } = props;
return (
<div className="flex-1 flex">
<form className="w-full flex md:ml-0" autoComplete="off" method="post" action="">
<input autoComplete="false" name="hidden" type="text" className="hidden" />
<label htmlFor="search-field" className="sr-only">
Search
</label>
<div className="relative w-full text-gray-400 focus-within:text-gray-400">
<div className="absolute inset-y-0 left-0 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5" aria-hidden="true" />
</div>
<input
id="search-field"
className="dark:bg-gray-800 block w-full h-full pl-8 pr-3 py-2 border-transparent dark:text-white text-gray-900 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-0 focus:border-transparent sm:text-sm"
placeholder="Search"
type="input"
name="search"
onChange={onSearchChange}
/>
</div>
</form>
</div>
)
}
export default Search;

@ -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 (
<>
<div className="md:flex md:w-16 md:flex-col md:fixed md:inset-y-0">
<div className="flex flex-col flex-grow pt-5 bg-blue-700 overflow-y-auto">
<div className="flex items-center flex-shrink-0 mx-auto">
<Link to={isAuthenticated ? "/" : "projects/new"}>
<Logo className="" />
</Link>
</div>
<div className="mt-5 flex-1 flex flex-col">
<nav className="flex-1 px-2 pb-4 space-y-1">
{navigation.map((item) => (
<a
key={item.name}
href={item.href}
className={classNames(
item.current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600",
"group flex items-center justify-center px-2 py-2 text-sm font-medium rounded-md"
)}
>
<item.icon className="mr-3 md:mr-0 flex-shrink-0 h-5 w-5" aria-hidden="true" />
<span className="md:hidden">
{item.name}
</span>
</a>
))}
</nav>
</div>
<UserMenu username={userName} current={pathname.includes("profile")} />
</div>
</div>
</>
);
}

@ -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
`}
>
<div className="flex items-center">
<div>
<UserCircleIcon className="inline-block h-8 w-8 rounded-full" />
</div>
<div className="ml-3">
<p className="text-base font-medium text-white">{username}</p>
<p className="text-sm font-medium text-indigo-200 group-hover:text-white">View profile</p>
<div className="flex items-center mx-auto">
<UserCircleIcon className="inline-block h-8 w-8 rounded-full text-white" />
<div className="ml-3 md:ml-0">
<p className="text-base font-medium text-white md:hidden">
{username
? <>{username}</>
: <>Log in</>
}
</p>
<p className="text-sm font-medium text-indigo-200 group-hover:text-white md:hidden">View profile</p>
</div>
</div>
</div>

@ -0,0 +1,28 @@
interface ILogoProps {
className: string;
}
const Logo = (props: ILogoProps) => {
const { className } = props;
return (
<svg width="28px" height="28px" viewBox="0 0 152 152" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink">
<title>Nuxx</title>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="Logo" transform="translate(-179.000000, -225.000000)">
<g id="nuxx-logo-copy" transform="translate(167.000000, 200.000000)">
<g id="nuxx-logo" transform="translate(98.500000, 95.000000) rotate(-60.000000) translate(-98.500000, -95.000000) translate(33.000000, 20.000000)">
<polygon id="Polygon" fillOpacity="0.15" fill="#000000" points="44 0 87.3012702 25 87.3012702 75 44 100 0.698729811 75 0.698729811 25"></polygon>
<polygon id="Polygon-Copy" fillOpacity="0.15" fill="#000000" points="87 25 130.30127 50 130.30127 100 87 125 43.6987298 100 43.6987298 50"></polygon>
<polygon id="Polygon-Copy-2" fillOpacity="0.15" fill="#000000" points="44 50 87.3012702 75 87.3012702 125 44 150 0.698729811 125 0.698729811 75"></polygon>
<text id="n" transform="translate(61.705647, 69.842201) rotate(30.000000) translate(-61.705647, -69.842201) " fontFamily="Kefa-Bold, Kefa" fontSize="71" fontWeight="bold" letterSpacing="0.492226294" fill="#FFFFFF">
<tspan x="39.6707155" y="94.8422015">n</tspan>
</text>
</g>
</g>
</g>
</g>
</svg>
)
}
export default Logo;

@ -1,20 +0,0 @@
interface ILogoProps {
className: string;
}
const Logo = (props: ILogoProps) => {
const { className } = props;
return (
<svg width="689" height="689" viewBox="0 0 689 689" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path opacity="0.8" d="M191.04 268.58H0.154419L0.154419 459.465H191.04V268.58Z" fill="white" />
<path opacity="0.8" d="M191.04 497.306H0.154419L0.154419 688.192H191.04V497.306Z" fill="white" />
<path opacity="0.8" d="M191.04 39.8536L0.154419 39.8536L0.154419 230.739H191.04V39.8536Z" fill="#4F95FF" />
<path opacity="0.8" d="M419.766 268.58H228.881V459.465H419.766V268.58Z" fill="#4F95FF" />
<path opacity="0.8" d="M419.766 497.306H228.881V688.192H419.766V497.306Z" fill="#4F95FF" />
<path opacity="0.8" d="M648.493 497.306H457.607V688.192H648.493V497.306Z" fill="white" />
<path opacity="0.8" d="M688.084 135.105L553.109 0.130157L418.134 135.105L553.109 270.081L688.084 135.105Z" fill="white" />
</svg>
)
}
export default Logo;

@ -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,10 +185,16 @@ export const useJsPlumb = (
onConnectionDetached([params.sourceId, firstConnection.suspendedElementId]);
if (params.targetId !== firstConnection.suspendedElementId) {
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;
}
}
}
// prevent duplicate connections from the same source to target
if (firstConnection.sourceId === params.sourceId && firstConnection.targetId === params.targetId) {
@ -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;

@ -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';

@ -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);
}
}
)
}

@ -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
}
)
}

@ -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}`
}
});
}

@ -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[];

@ -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;
}

Loading…
Cancel
Save