feat: yaml importing

pull/94/head
corpulent 3 years ago
parent a0d456079b
commit f37c271581

@ -15,3 +15,5 @@ boto3==1.21.46
requests==2.27.1
pyaml==21.10.1
ruamel.yaml==0.17.21
networkx==2.8.5
numpy==1.23.1

@ -5,6 +5,7 @@ from .models import Project
class ProjectAdmin(admin.ModelAdmin):
list_display = (
'id',
'visibility',
'name',
'uuid',
'created_at',

@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-08-04 06:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_project_uuid'),
]
operations = [
migrations.AddField(
model_name='project',
name='visibility',
field=models.SmallIntegerField(default='1'),
),
]

@ -10,6 +10,7 @@ class Project(models.Model):
related_name="projects",
on_delete=models.CASCADE,
)
visibility = models.SmallIntegerField(blank=False, null=False, default="1")
name = models.CharField(max_length=500, blank=False, null=False, default="Untitled")
uuid = models.CharField(max_length=500, blank=True, null=True, unique=True)
data = models.TextField(blank=False)

@ -15,6 +15,7 @@ class DefaultRouterPlusPlus(ExtendedDefaultRouter):
api_urls = [
path("", view.ViewGenericAPIView.as_view()),
path("projects/", project.ProjectListCreateAPIView.as_view()),
path("projects/import/", project.ProjectImportAPIView.as_view()),
path("projects/<str:uuid>/", project.ProjectGenericAPIView.as_view()),
path("generate/", generate.GenerateGenericAPIView.as_view()),
path("auth/self/", user.UserGenericAPIView.as_view()),

@ -25,7 +25,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "django-insecure--i+bd*fda@!=_0yv$((3(@nruqvv(8c1c8no^+yjl%@b859f57"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = os.getenv(
'DEBUG',
'true').lower() == 'true'
ALLOWED_HOSTS = ["*"]

@ -0,0 +1,47 @@
import * as yup from "yup";
export interface IImportForm {
url: string;
visibility: string[];
}
export interface IImportFinalValues {
url: string;
visibility: number;
}
const initialValues: IImportForm = {
url: "",
visibility: []
};
export const validationSchema = yup.object({
url: yup
.string()
.max(256, "url should be 500 characters or less")
.required("url is required")
});
export const getInitialValues = (values?: any): IImportForm => {
if (!values) {
return {
...initialValues
};
}
const { url, visibility } = values;
return {
url: url ?? (initialValues.url as string),
visibility: visibility ?? []
};
};
export const getFinalValues = (values: IImportForm): IImportFinalValues => {
const { url, visibility } = values;
return {
url: url ?? "",
visibility: visibility.length ? 1 : 0
};
};

@ -0,0 +1,115 @@
import { useCallback, useMemo, useState } from "react";
import { Field, Formik } from "formik";
import { styled } from "@mui/joy";
import { XIcon } from "@heroicons/react/outline";
import { CallbackFunction } from "../../../types";
import { IImportForm } from "./form-utils";
import {
getFinalValues,
getInitialValues,
validationSchema
} from "./form-utils";
import TextField from "../../global/FormElements/TextField";
import { toaster } from "../../../utils";
import { reportErrorsAndSubmit } from "../../../utils/forms";
import { ScrollView } from "../../ScrollView";
import lodash from "lodash";
interface IModalImportProps {
onHide: CallbackFunction;
onImport: CallbackFunction;
importing: boolean;
}
const FormContainer = styled("div")`
display: flex;
flex-direction: column;
justify-content: space-between;
`;
const ModalImport = (props: IModalImportProps) => {
const { onHide, onImport, importing } = props;
const handleCreate = useCallback(
(values: IImportForm, formik: any) => {
const result = getFinalValues(values);
onImport(result);
toaster(`Importing...`, "success");
},
[onImport, onHide]
);
const initialValues = useMemo(() => getInitialValues(), []);
return (
<div className="fixed z-50 inset-0 overflow-y-auto">
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
<div
onClick={onHide}
className="opacity-25 fixed inset-0 z-40 bg-black"
></div>
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
<h3 className="text-sm font-semibold">Import</h3>
<button
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
onClick={onHide}
>
<span className="block outline-none focus:outline-none">
<XIcon className="w-4" />
</span>
</button>
</div>
<Formik
initialValues={initialValues}
enableReinitialize={true}
onSubmit={handleCreate}
validationSchema={validationSchema}
>
{(formik) => (
<FormContainer>
<div>
<ScrollView
className="relative px-4 py-3 flex-auto"
height={""}
>
<TextField label="yaml url" name="url" required={true} />
<label htmlFor="visibility" className="lbl-util">
Visibility
</label>
<input
id="visibility"
name="visibility"
className="checkbox-util"
type="checkbox"
onChange={formik.handleChange}
/>
{importing && <>importing</>}
</ScrollView>
</div>
<div className="flex items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b">
<button
className="btn-util"
type="button"
onClick={reportErrorsAndSubmit(formik)}
>
Import
</button>
</div>
</FormContainer>
)}
</Formik>
</div>
</div>
</div>
</div>
);
};
export default ModalImport;

@ -40,6 +40,7 @@ import CreateVolumeModal from "../Modal/volume/CreateVolumeModal";
import EditVolumeModal from "../Modal/volume/EditVolumeModal";
import CodeEditor from "../CodeEditor";
import { useTitle } from "../../hooks";
import VisibilitySwitch from "../global/VisibilitySwitch";
export default function Project() {
const { uuid } = useParams<{ uuid: string }>();
@ -50,6 +51,7 @@ export default function Project() {
const stateConnectionsRef = useRef<[[string, string]] | []>();
const stateNetworksRef = useRef({});
const [isVisible, setIsVisible] = useState(false);
const [generatedCode, setGeneratedCode] = useState<string>();
const [formattedCode, setFormattedCode] = useState<string>("");
const [showModalCreateService, setShowModalCreateService] = useState(false);
@ -126,6 +128,7 @@ export default function Project() {
const onSave = () => {
const payload: IProjectPayload = {
name: projectName,
visibility: +isVisible,
data: {
canvas: {
position: canvasPosition,
@ -172,6 +175,7 @@ export default function Project() {
);
setProjectName(data.name);
setIsVisible(Boolean(data.visibility));
setNodes(clientNodeItems);
setConnections(canvasData.canvas.connections);
setNetworks(canvasData.canvas.networks);
@ -463,17 +467,12 @@ export default function Project() {
/>
<div className="flex flex-col space-y-2 w-full justify-end mb-4 md:flex-row md:space-y-0 md:space-x-2 md:mb-0">
<button
onClick={() => {
window.location.replace("/projects/new");
<VisibilitySwitch
isVisible={isVisible}
onToggle={() => {
setIsVisible(!isVisible);
}}
type="button"
className="btn-util text-black bg-gray-200 hover:bg-gray-300 sm:w-auto"
>
<div className="flex justify-center items-center space-x-2 mx-auto">
<span>New project</span>
</div>
</button>
/>
<button
onClick={() => onSave()}

@ -1,6 +1,6 @@
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { PencilIcon, TrashIcon } from "@heroicons/react/outline";
import { useNavigate } from "react-router-dom";
import { TrashIcon } from "@heroicons/react/outline";
import { truncateStr } from "../../utils";
import { IProject } from "../../types";
import ModalConfirmDelete from "../../components/Modal/ConfirmDelete";

@ -1,35 +1,78 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { PROJECTS_FETCH_LIMIT } from "../../constants";
import ModalImport from "../Modal/import";
import { IProject } from "../../types";
import { toaster } from "../../utils";
import Spinner from "../../components/global/Spinner";
import PreviewBlock from "./PreviewBlock";
import { useProjects } from "../../hooks/useProjects";
import { PlusIcon } from "@heroicons/react/outline";
import { importProject } from "../../hooks/useImportProject";
import { IImportFinalValues } from "../Modal/import/form-utils";
const Projects = () => {
const navigate = useNavigate();
const [limit] = useState(PROJECTS_FETCH_LIMIT);
const [offset, setOffset] = useState(0);
const [importing, setImporting] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const { isLoading, isError, error, data, isFetching, isPreviousData } =
useProjects(limit, offset);
const onImportClick = () => {
setShowImportModal(true);
};
const handleImport = (values: IImportFinalValues) => {
setImporting(true);
importProject(values)
.then((resp: any) => {
navigate(`/projects/${resp.name}`);
toaster(`Imported!`, "success");
})
.catch((e: any) => {
toaster(`Something went wrong!`, "error");
})
.finally(() => {
setImporting(false);
});
};
return (
<>
{showImportModal ? (
<ModalImport
onHide={() => setShowImportModal(false)}
onImport={(values: IImportFinalValues) => handleImport(values)}
importing={importing}
/>
) : null}
<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">
<div className="flex flex-col sm:flex-row justify-between px-4 sm:px-6 md:px-8">
<h1 className="text-2xl font-semibold dark:text-white text-gray-900">
Projects
</h1>
{data && data.results.length > 0 && (
<div className="flex justify-end space-x-1">
<button
onClick={onImportClick}
className="btn-util text-white bg-blue-600 hover:bg-blue-700 sm:w-auto"
>
<span>Import</span>
</button>
<Link
className="btn-util text-white bg-blue-600 hover:bg-blue-700 sm:w-auto"
to="/projects/new"
>
<span>Create new project</span>
</Link>
</div>
)}
</div>

@ -1,5 +1,5 @@
import { useLocation } from "react-router-dom";
import { BookOpenIcon } from "@heroicons/react/outline";
import { BookOpenIcon, PlusIcon } from "@heroicons/react/outline";
import { Link } from "react-router-dom";
import UserMenu from "./UserMenu";
import Logo from "./logo";
@ -20,6 +20,12 @@ export default function SideBar(props: ISideBarProps) {
href: "/projects",
icon: BookOpenIcon,
current: pathname.match(projRegex) ? true : false
},
{
name: "New project",
href: "/projects/new",
icon: PlusIcon,
current: false
}
];
@ -36,7 +42,7 @@ export default function SideBar(props: ISideBarProps) {
</div>
<div className="md:mt-5 flex-1 flex flex-col items-center sm:flex-row md:flex-col justify-end">
<nav className="md:flex-1 space-y-1">
<nav className="flex md:flex-1 md:flex-col items-center md:space-y-1">
{navigation.map((item) => (
<a
key={item.name}

@ -0,0 +1,38 @@
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
import { CallbackFunction } from "../../../types";
interface IVisibilitySwitchProps {
onToggle: CallbackFunction;
isVisible: boolean;
}
const VisibilitySwitch = (props: IVisibilitySwitchProps) => {
const { isVisible, onToggle } = props;
return (
<div className="flex flex items-center justify-end">
<button
onClick={onToggle}
id="theme-toggle"
type="button"
className="
btn-util
bg-white
focus:ring-0
text-gray-500
hover:bg-white
hover:text-gray-800
focus:outline-none
text-sm"
>
{isVisible ? (
<EyeIcon id="theme-toggle-light-icon" className="w-5 h-5" />
) : (
<EyeOffIcon id="theme-toggle-dark-icon" className="w-5 h-5" />
)}
</button>
</div>
);
};
export default VisibilitySwitch;

@ -0,0 +1,18 @@
import axios from "axios";
import { IImportFinalValues } from "../components/Modal/import/form-utils";
import { API_SERVER_URL } from "../constants";
import { getLocalStorageJWTKeys } from "../utils";
export const importProject = async (values: IImportFinalValues) => {
const jwtKeys = getLocalStorageJWTKeys();
const response = await axios({
method: "post",
url: `${API_SERVER_URL}/projects/import/`,
data: { ...values },
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwtKeys.access_token}`
}
});
return response.data;
};

@ -125,6 +125,9 @@ path,
.input-util {
@apply shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md px-2 py-1
}
.checkbox-util {
@apply shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block sm:text-sm border-gray-300 rounded-md px-2 py-1
}
}
code {

@ -337,6 +337,7 @@ export interface INetworkNodeItem extends INodeItem {
export interface IProjectPayload {
name: string;
visibility: number;
data: {
canvas: {
position: {

Loading…
Cancel
Save