From 0660313484c63f5e22ef044d2b53d84547feb26b Mon Sep 17 00:00:00 2001 From: prosfus Date: Wed, 23 Oct 2024 18:10:49 +0200 Subject: [PATCH] feat: create service with FDL --- package.json | 8 +- src/components/RequestButton/index.tsx | 2 + src/components/ui/dialog.tsx | 47 ++--- src/components/ui/tabs.tsx | 53 +++++ src/lib/axiosClient.ts | 8 +- .../ui/services/components/FDL/index.tsx | 191 ++++++++++++++++++ .../Topbar/components/CreateServiceButton.tsx | 12 +- .../ui/services/components/Topbar/index.tsx | 1 + .../ui/services/context/ServicesContext.tsx | 9 + src/pages/ui/services/router.tsx | 2 + vite.config.ts | 10 +- 11 files changed, 312 insertions(+), 31 deletions(-) create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/pages/ui/services/components/FDL/index.tsx diff --git a/package.json b/package.json index d9d115d..b35361f 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.654.0", + "@bithero/monaco-editor-vite-plugin": "^1.0.2", + "@monaco-editor/react": "^4.6.0", "@radix-ui/react-checkbox": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", @@ -23,6 +25,7 @@ "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.2", "axios": "^1.7.2", "class-variance-authority": "^0.7.0", @@ -37,7 +40,8 @@ "react-use-measure": "^2.1.1", "sonner": "^1.5.0", "tailwind-merge": "^2.4.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "yaml": "^2.6.0" }, "devDependencies": { "@types/node": "^20.14.11", diff --git a/src/components/RequestButton/index.tsx b/src/components/RequestButton/index.tsx index bf62721..8499a7f 100644 --- a/src/components/RequestButton/index.tsx +++ b/src/components/RequestButton/index.tsx @@ -27,6 +27,8 @@ function RequestButton({ const [ref, bounds] = useMeasure(); async function onClick() { + if (props.disabled) return; + if (isLoading) return; // Evita mĂșltiples clics setIsLoading(true); await request(); setIsLoading(false); diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 306cfc3..47dcfe1 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,16 +1,16 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -24,19 +24,20 @@ const DialogOverlay = React.forwardRef< )} {...props} /> -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( +>(({ className, children, style, ...props }, ref) => ( -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -62,8 +63,8 @@ const DialogHeader = ({ )} {...props} /> -) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -76,8 +77,8 @@ const DialogFooter = ({ )} {...props} /> -) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -91,8 +92,8 @@ const DialogTitle = React.forwardRef< )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -103,8 +104,8 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-slate-500 dark:text-slate-400", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -117,4 +118,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} +}; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..9588e7f --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/lib/axiosClient.ts b/src/lib/axiosClient.ts index 24efa0d..bdf28db 100644 --- a/src/lib/axiosClient.ts +++ b/src/lib/axiosClient.ts @@ -2,8 +2,8 @@ import { AuthData } from "@/contexts/AuthContext"; import axios from "axios"; export function setAxiosInterceptor(authData: AuthData) { - const { endpoint, user: username, password,token } = authData; - if ( token === undefined) { + const { endpoint, user: username, password, token } = authData; + if (token === undefined) { axios.interceptors.request.use((config) => { config.baseURL = endpoint; config.auth = { @@ -12,10 +12,10 @@ export function setAxiosInterceptor(authData: AuthData) { }; return config; }); - }else{ + } else { axios.interceptors.request.use((config) => { config.baseURL = endpoint; - config.headers.Authorization = "Bearer "+token + config.headers.Authorization = "Bearer " + token; return config; }); } diff --git a/src/pages/ui/services/components/FDL/index.tsx b/src/pages/ui/services/components/FDL/index.tsx new file mode 100644 index 0000000..c84f64d --- /dev/null +++ b/src/pages/ui/services/components/FDL/index.tsx @@ -0,0 +1,191 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import useServicesContext from "../../context/ServicesContext"; +import { + Dialog, + DialogDescription, + DialogTitle, + DialogContent, + DialogHeader, + DialogFooter, +} from "@/components/ui/dialog"; +import { useState, useEffect } from "react"; +import Editor from "@monaco-editor/react"; +import { Input } from "@/components/ui/input"; +import YAML from "yaml"; +import { Service } from "../../models/service"; +import createServiceApi from "@/api/services/createServiceApi"; +import { alert } from "@/lib/alert"; +import RequestButton from "@/components/RequestButton"; + +function FDLForm() { + const { showFDLModal, setShowFDLModal, refreshServices } = + useServicesContext(); + const [selectedTab, setSelectedTab] = useState<"fdl" | "script">("fdl"); + const [editorKey, setEditorKey] = useState(0); + + const [fdl, setFdl] = useState(""); + const [script, setScript] = useState(""); + + function handleFileUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + setFdl(e.target?.result as string); + }; + reader.readAsText(file); + } + + const prepareServices = () => { + const obj = YAML.parse(fdl); + const services: Service[] = []; + const scriptContent = script; + if (obj.functions && obj.functions.oscar) { + obj.functions.oscar.forEach((service: Record) => { + console.log(service); + const serviceKey = Object.keys(service)[0]; + const serviceParams = service[serviceKey]; + serviceParams.script = scriptContent; + serviceParams.storage_providers = obj.storage_providers || {}; + serviceParams.clusters = obj.clusters || {}; + services.push(serviceParams); + }); + } + + return services; + }; + + async function handleSave() { + if (!fdl) { + alert.error("Please fill the FDL file"); + return; + } + + if (!script) { + alert.error("Please fill the script"); + return; + } + + const services = prepareServices(); + + const promises = services.map(async (service) => { + const response = await createServiceApi(service); + return response; + }); + + const results = await Promise.allSettled(promises); + + results.forEach((result, index) => { + if (result.status === "rejected") { + alert.error( + `Error creating service ${services[index].name}: ${result.reason}` + ); + } else { + alert.success(`Service ${services[index].name} created successfully`); + } + }); + + if (results.every((result) => result.status === "fulfilled")) { + setShowFDLModal(false); + setFdl(""); + setScript(""); + setSelectedTab("fdl"); + refreshServices(); + } + } + + useEffect(() => { + const handleResize = () => { + setEditorKey((prevKey) => prevKey + 1); + }; + + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + useEffect(() => { + if (!showFDLModal) { + setFdl(""); + setScript(""); + setSelectedTab("fdl"); + } + }, [showFDLModal]); + + return ( + + {/* Open */} + + + Create the service using FDL + + Use the code editor to edit the FDL file and the script. + + + { + setSelectedTab(value as "fdl" | "script"); + }} + > +
+ + + FDL + + + Script + + + + +
+ + { + setFdl(e || ""); + }} + width="100%" + height="60vh" + options={{ + minimap: { + enabled: false, + }, + }} + /> + + + { + setScript(e || ""); + }} + width="100%" + height="60vh" + options={{ + minimap: { + enabled: false, + }, + }} + /> + +
+ + Save + +
+
+ ); +} + +export default FDLForm; diff --git a/src/pages/ui/services/components/Topbar/components/CreateServiceButton.tsx b/src/pages/ui/services/components/Topbar/components/CreateServiceButton.tsx index 3887821..f32b8da 100644 --- a/src/pages/ui/services/components/Topbar/components/CreateServiceButton.tsx +++ b/src/pages/ui/services/components/Topbar/components/CreateServiceButton.tsx @@ -8,12 +8,14 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Plus, Settings } from "lucide-react"; +import { FileCode, Plus, Settings } from "lucide-react"; import { useMediaQuery } from "react-responsive"; import { useNavigate } from "react-router-dom"; +import useServicesContext from "../../../context/ServicesContext"; function AddServiceButton() { const navigate = useNavigate(); + const { setShowFDLModal } = useServicesContext(); const isSmallScreen = useMediaQuery({ maxWidth: 799 }); return ( @@ -42,6 +44,14 @@ function AddServiceButton() { Form + { + setShowFDLModal(true); + }} + > + + FDL + diff --git a/src/pages/ui/services/components/Topbar/index.tsx b/src/pages/ui/services/components/Topbar/index.tsx index 5a05a89..cff8a2a 100644 --- a/src/pages/ui/services/components/Topbar/index.tsx +++ b/src/pages/ui/services/components/Topbar/index.tsx @@ -17,6 +17,7 @@ export enum ServiceViewMode { function ServicesTopbar() { const location = useLocation(); const pathnames = location.pathname.split("/").filter((x) => x && x !== "ui"); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_, serviceId] = pathnames; useEffect(() => { diff --git a/src/pages/ui/services/context/ServicesContext.tsx b/src/pages/ui/services/context/ServicesContext.tsx index 8b13617..6973450 100644 --- a/src/pages/ui/services/context/ServicesContext.tsx +++ b/src/pages/ui/services/context/ServicesContext.tsx @@ -33,6 +33,11 @@ interface ServiceContextType { formService: Service; setFormService: Dispatch>; + + showFDLModal: boolean; + setShowFDLModal: Dispatch>; + + refreshServices: () => void; } export const ServicesContext = createContext({ @@ -45,6 +50,7 @@ export const ServicesProvider = ({ children: React.ReactNode; }) => { const [services, setServices] = useState([] as Service[]); + const [showFDLModal, setShowFDLModal] = useState(false); const { serviceId } = useParams(); // Filter and order TABLE rows @@ -114,6 +120,9 @@ export const ServicesProvider = ({ setFormTab, formService, setFormService, + showFDLModal, + setShowFDLModal, + refreshServices: handleGetServices, }} > {children} diff --git a/src/pages/ui/services/router.tsx b/src/pages/ui/services/router.tsx index ead644f..8507a18 100644 --- a/src/pages/ui/services/router.tsx +++ b/src/pages/ui/services/router.tsx @@ -3,6 +3,7 @@ import ServicesTopbar from "./components/Topbar"; import ServicesList from "./components/ServicesList"; import { ServicesProvider } from "./context/ServicesContext"; import ServiceForm from "./components/ServiceForm"; +import FDLForm from "./components/FDL"; function ServicesRouter() { return ( @@ -20,6 +21,7 @@ function ServicesRouter() { }} > + diff --git a/vite.config.ts b/vite.config.ts index a745253..3bcf066 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,17 @@ import path from "path"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +import { monaco } from "@bithero/monaco-editor-vite-plugin"; export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + monaco({ + features: "all", + languages: ["yaml", "javascript"], + globalAPI: true, + }), + ], resolve: { alias: { "@": path.resolve(__dirname, "./src"),