diff --git a/deploy-web/src/components/home/YourAccount.tsx b/deploy-web/src/components/home/YourAccount.tsx index 81da93175..793fd53cf 100644 --- a/deploy-web/src/components/home/YourAccount.tsx +++ b/deploy-web/src/components/home/YourAccount.tsx @@ -88,7 +88,7 @@ export const YourAccount: React.FunctionComponent = ({ balances, isLoadin const totalStorage = activeDeployments.map(d => d.storageAmount).reduce((a, b) => a + b, 0); const _ram = bytesToShrink(totalMemory); const _storage = bytesToShrink(totalStorage); - const [deploySdl, setDeploySdl] = useAtom(sdlStore.deploySdl); + const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const { price, isLoaded } = usePricing(); const colors = { diff --git a/deploy-web/src/components/layout/Sidebar.tsx b/deploy-web/src/components/layout/Sidebar.tsx index b5bd4350f..cf3027b9f 100644 --- a/deploy-web/src/components/layout/Sidebar.tsx +++ b/deploy-web/src/components/layout/Sidebar.tsx @@ -94,7 +94,7 @@ export const Sidebar: React.FunctionComponent = ({ isMobileOpen, handleDr const theme = useTheme(); const [isHovering, setIsHovering] = useState(false); const _isNavOpen = isNavOpen || isHovering; - const [deploySdl, setDeploySdl] = useAtom(sdlStore.deploySdl); + const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const smallScreen = useMediaQuery(theme.breakpoints.down("md")); const routeGroups = [ diff --git a/deploy-web/src/components/layout/WalletStatus.tsx b/deploy-web/src/components/layout/WalletStatus.tsx index c2aec9f2f..f573f5e3f 100644 --- a/deploy-web/src/components/layout/WalletStatus.tsx +++ b/deploy-web/src/components/layout/WalletStatus.tsx @@ -14,11 +14,11 @@ import { Address } from "../shared/Address"; import { CustomMenuItem } from "../shared/CustomMenuItem"; import ExitToAppIcon from "@mui/icons-material/ExitToApp"; import AccountBalanceIcon from "@mui/icons-material/AccountBalance"; -import { GrantModal } from "../wallet/GrantModal"; import Link from "next/link"; import { UrlService } from "@src/utils/urlUtils"; import { FormattedNumber } from "react-intl"; import { useTotalWalletBalance } from "@src/hooks/useWalletBalance"; +import { useRouter } from "next/router"; type Props = { children?: ReactNode; @@ -39,8 +39,8 @@ export const WalletStatus: React.FunctionComponent = ({}) => { const popupState = usePopupState({ variant: "popover", popupId: "walletMenu" }); const { classes } = useStyles(); const { isWalletConnected, walletName, address, walletBalances, logout, isWalletLoaded } = useWallet(); - const [isShowingGrantModal, setIsShowingGrantModal] = useState(false); const walletBalance = useTotalWalletBalance(); + const router = useRouter(); function onDisconnectClick() { popupState.close(); @@ -51,7 +51,7 @@ export const WalletStatus: React.FunctionComponent = ({}) => { const onAuthorizeSpendingClick = () => { popupState.close(); - setIsShowingGrantModal(true); + router.push(UrlService.settingsAuthorizations()); }; return ( @@ -127,9 +127,6 @@ export const WalletStatus: React.FunctionComponent = ({}) => { )} - - {isShowingGrantModal && setIsShowingGrantModal(false)} />} ); }; - diff --git a/deploy-web/src/components/newDeploymentWizard/CreateLease.tsx b/deploy-web/src/components/newDeploymentWizard/CreateLease.tsx index da1c50d2e..364a579ec 100644 --- a/deploy-web/src/components/newDeploymentWizard/CreateLease.tsx +++ b/deploy-web/src/components/newDeploymentWizard/CreateLease.tsx @@ -164,7 +164,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { setFilteredBids(fBids); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [search, bids, providers, isFilteringFavorites, isFilteringAudited]); + }, [search, bids, providers, isFilteringFavorites, isFilteringAudited, favoriteProviders]); const handleBidSelected = bid => { setSelectedBids({ ...selectedBids, [bid.gseq]: bid }); @@ -476,4 +476,3 @@ export const CreateLease: React.FunctionComponent = ({ dseq }) => { ); }; - diff --git a/deploy-web/src/components/newDeploymentWizard/ManifestEdit.tsx b/deploy-web/src/components/newDeploymentWizard/ManifestEdit.tsx index 93d509363..d6d6e13b7 100644 --- a/deploy-web/src/components/newDeploymentWizard/ManifestEdit.tsx +++ b/deploy-web/src/components/newDeploymentWizard/ManifestEdit.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, Dispatch } from "react"; -import { Box, Typography, Button, TextField, CircularProgress, Tooltip, Alert, useMediaQuery, useTheme } from "@mui/material"; +import { useState, useEffect, Dispatch, useRef } from "react"; +import { Box, Typography, Button, TextField, CircularProgress, Tooltip, Alert, useMediaQuery, useTheme, ButtonGroup } from "@mui/material"; import InfoIcon from "@mui/icons-material/Info"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForward"; import { useSettings } from "../../context/SettingsProvider"; @@ -26,6 +26,7 @@ import { generateCertificate } from "@src/utils/certificateUtils"; import { updateWallet } from "@src/utils/walletUtils"; import sdlStore from "@src/store/sdlStore"; import { useAtom } from "jotai"; +import { SdlBuilder, SdlBuilderRefType } from "./SdlBuilder"; const yaml = require("js-yaml"); @@ -55,20 +56,23 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s const [isCreatingDeployment, setIsCreatingDeployment] = useState(false); const [isDepositingDeployment, setIsDepositingDeployment] = useState(false); const [isCheckingPrerequisites, setIsCheckingPrerequisites] = useState(false); + const [selectedSdlEditMode, setSelectedSdlEditMode] = useAtom(sdlStore.selectedSdlEditMode); const [sdlDenom, setSdlDenom] = useState("uakt"); const { settings } = useSettings(); const { address, signAndBroadcastTx } = useWallet(); const router = useRouter(); const { classes } = useStyles(); const { loadValidCertificates, localCert, isLocalCertMatching, loadLocalCert, setSelectedCertificate } = useCertificate(); - const [deploySdl, setDeploySdl] = useAtom(sdlStore.deploySdl); + const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const theme = useTheme(); const smallScreen = useMediaQuery(theme.breakpoints.down("md")); + const sdlBuilderRef = useRef(null); useEffect(() => { if (selectedTemplate?.name) { setDeploymentName(selectedTemplate.name); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -158,7 +162,9 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s async function handleCreateClick(deposit: number, depositorAddress: string) { setIsCreatingDeployment(true); - const dd = await createAndValidateDeploymentData(editedManifest, null, deposit, depositorAddress); + + const sdl = selectedSdlEditMode === "yaml" ? editedManifest : sdlBuilderRef.current?.getSdl(); + const dd = await createAndValidateDeploymentData(sdl, null, deposit, depositorAddress); const validCertificates = await loadValidCertificates(); const currentCert = validCertificates.find(x => x.parsed === localCert?.certPem); @@ -203,7 +209,7 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s setDeploySdl(null); // Save the manifest - saveDeploymentManifestAndName(dd.deploymentId.dseq, editedManifest, dd.version, address, deploymentName); + saveDeploymentManifestAndName(dd.deploymentId.dseq, sdl, dd.version, address, deploymentName); router.replace(UrlService.newDeployment({ step: RouteStepKeys.createLeases, dseq: dd.deploymentId.dseq })); event(AnalyticsEvents.CREATE_DEPLOYMENT, { @@ -219,6 +225,17 @@ export const ManifestEdit: React.FunctionComponent = ({ editedManifest, s } } + const onModeChange = (mode: "yaml" | "builder") => { + if (mode === selectedSdlEditMode) return; + + if (mode === "yaml") { + const sdl = sdlBuilderRef.current?.getSdl(); + setEditedManifest(sdl); + } + + setSelectedSdlEditMode(mode); + }; + return ( <> = ({ editedManifest, s url={`https://deploy.cloudmos.io${UrlService.newDeployment({ step: RouteStepKeys.editDeployment })}`} /> - - - setDeploymentName(ev.target.value)} - fullWidth - label="Name your deployment (optional)" - variant="outlined" - size="small" - /> + + + + setDeploymentName(ev.target.value)} + fullWidth + label="Name your deployment (optional)" + variant="outlined" + size="small" + /> + + + + + + You may use the sample deployment file as-is or modify it for your own needs as described in the{" "} + handleDocClick(ev, "https://docs.akash.network/intro-to-akash/stack-definition-language")}> + SDL (Stack Definition Language) + {" "} + documentation. A typical modification would be to reference your own image instead of the demo app image. + + + } + > + + + + + - - - - You may use the sample deployment file as-is or modify it for your own needs as described in the{" "} - handleDocClick(ev, "https://docs.akash.network/intro-to-akash/stack-definition-language")}> - SDL (Stack Definition Language) - {" "} - documentation. A typical modification would be to reference your own image instead of the demo app image. - - - } + + - + {parsingError && {parsingError}} - - - + {selectedSdlEditMode === "yaml" && ( + + + + )} + {selectedSdlEditMode === "builder" && } {isDepositingDeployment && ( = ({ editedManifest, s ); }; - diff --git a/deploy-web/src/components/newDeploymentWizard/SdlBuilder.tsx b/deploy-web/src/components/newDeploymentWizard/SdlBuilder.tsx new file mode 100644 index 000000000..0acce7fde --- /dev/null +++ b/deploy-web/src/components/newDeploymentWizard/SdlBuilder.tsx @@ -0,0 +1,142 @@ +import React, { Dispatch, useEffect, useRef, useState } from "react"; +import { defaultService } from "@src/utils/sdl/data"; +import { useFieldArray, useForm } from "react-hook-form"; +import { SdlBuilderFormValues, Service } from "@src/types"; +import { nanoid } from "nanoid"; +import { generateSdl } from "@src/utils/sdl/sdlGenerator"; +import { Alert, Box, Button, CircularProgress } from "@mui/material"; +import { SimpleServiceFormControl } from "../sdl/SimpleServiceFormControl"; +import { useProviderAttributesSchema } from "@src/queries/useProvidersQuery"; +import { importSimpleSdl } from "@src/utils/sdl/sdlImport"; +import { Subscription } from "react-hook-form/dist/utils/createSubject"; + +interface Props { + sdlString: string; + setEditedManifest: Dispatch; +} + +export type SdlBuilderRefType = { + getSdl: () => string; +}; + +export const SdlBuilder = React.forwardRef(({ sdlString, setEditedManifest }, ref) => { + const [error, setError] = useState(null); + const formRef = useRef(); + const [isInit, setIsInit] = useState(false); + const { control, trigger, watch, setValue } = useForm({ + defaultValues: { + services: [{ ...defaultService }] + } + }); + const { + fields: services, + remove: removeService, + append: appendService + } = useFieldArray({ + control, + name: "services", + keyName: "id" + }); + const { services: _services } = watch(); + const { data: providerAttributesSchema } = useProviderAttributesSchema(); + const [serviceCollapsed, setServiceCollapsed] = useState([]); + + React.useImperativeHandle(ref, () => ({ + getSdl: getSdl + })); + + useEffect(() => { + const { unsubscribe } = watch(data => { + const sdl = generateSdl({ services: data.services as Service[] }); + setEditedManifest(sdl); + }); + + try { + const services = createAndValidateSdl(sdlString); + setValue("services", services as Service[]); + } catch (error) { + setError("Error importing SDL"); + } + + setIsInit(true); + + return () => { + unsubscribe(); + }; + }, [watch]); + + const getSdl = () => { + return generateSdl({ services: _services }); + }; + + const createAndValidateSdl = (yamlStr: string) => { + try { + if (!yamlStr) return []; + + const services = importSimpleSdl(yamlStr, providerAttributesSchema); + + setError(null); + + return services; + } catch (err) { + if (err.name === "YAMLException" || err.name === "CustomValidationError") { + setError(err.message); + } else if (err.name === "TemplateValidation") { + setError(err.message); + } else { + setError("Error while parsing SDL file"); + // setParsingError(err.message); + console.error(err); + } + } + }; + + const onAddService = () => { + appendService({ ...defaultService, id: nanoid(), title: `service-${services.length + 1}` }); + }; + + const onRemoveService = (index: number) => { + removeService(index); + }; + + return ( + + {!isInit ? ( + + + + ) : ( +
+ {services.map((service, serviceIndex) => ( + + ))} + + {error && ( + + {error} + + )} + + +
+ +
+
+ + )} +
+ ); +}); diff --git a/deploy-web/src/components/newDeploymentWizard/TemplateList.tsx b/deploy-web/src/components/newDeploymentWizard/TemplateList.tsx index 8c758a71b..330381b96 100644 --- a/deploy-web/src/components/newDeploymentWizard/TemplateList.tsx +++ b/deploy-web/src/components/newDeploymentWizard/TemplateList.tsx @@ -34,9 +34,10 @@ const previewTemplateIds = [ type Props = { setSelectedTemplate: Dispatch; + setEditedManifest: Dispatch; }; -export const TemplateList: React.FunctionComponent = ({ setSelectedTemplate }) => { +export const TemplateList: React.FunctionComponent = ({ setSelectedTemplate, setEditedManifest }) => { const { templates } = useTemplates(); const { classes } = useStyles(); const router = useRouter(); @@ -52,6 +53,7 @@ export const TemplateList: React.FunctionComponent = ({ setSelectedTempla function selectTemplate(template) { setSelectedTemplate(template); + setEditedManifest(template.content); router.push(UrlService.newDeployment({ step: RouteStepKeys.editDeployment })); } @@ -71,6 +73,7 @@ export const TemplateList: React.FunctionComponent = ({ setSelectedTempla description: "Custom uploaded file", content: event.target.result as string }); + setEditedManifest(event.target.result as string); router.push(UrlService.newDeployment({ step: RouteStepKeys.editDeployment })); }; diff --git a/deploy-web/src/components/sdl/PreviewSdl.tsx b/deploy-web/src/components/sdl/PreviewSdl.tsx new file mode 100644 index 000000000..917e64661 --- /dev/null +++ b/deploy-web/src/components/sdl/PreviewSdl.tsx @@ -0,0 +1,57 @@ +import { ReactNode } from "react"; +import { Popup } from "../shared/Popup"; +import { Box, Button, IconButton, Typography, useTheme } from "@mui/material"; +import { useSnackbar } from "notistack"; +import Editor from "@monaco-editor/react"; +import { copyTextToClipboard } from "@src/utils/copyClipboard"; +import FileCopy from "@mui/icons-material/FileCopy"; +import { Snackbar } from "../shared/Snackbar"; + +type Props = { + sdl: string; + onClose: () => void; + children?: ReactNode; +}; + +export const PreviewSdl: React.FunctionComponent = ({ sdl, onClose }) => { + const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + + const onCopyClick = () => { + copyTextToClipboard(sdl); + enqueueSnackbar(, { + variant: "success", + autoHideDuration: 3000 + }); + }; + + return ( + + + + + + + + + ); +}; diff --git a/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx b/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx index 8e3a0f8ec..baa74ee36 100644 --- a/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx +++ b/deploy-web/src/components/sdl/SimpleSdlBuilderForm.tsx @@ -23,6 +23,7 @@ import sdlStore from "@src/store/sdlStore"; import { RouteStepKeys } from "@src/utils/constants"; import { useAtom } from "jotai"; import { useProviderAttributesSchema } from "@src/queries/useProvidersQuery"; +import { PreviewSdl } from "./PreviewSdl"; type Props = {}; @@ -33,9 +34,11 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { const [isLoadingTemplate, setIsLoadingTemplate] = useState(false); const [isSavingTemplate, setIsSavingTemplate] = useState(false); const [isImportingSdl, setIsImportingSdl] = useState(false); - const [, setSdlResult] = useState(null); + const [isPreviewingSdl, setIsPreviewingSdl] = useState(false); + const [sdlResult, setSdlResult] = useState(null); const formRef = useRef(); const [, setDeploySdl] = useAtom(sdlStore.deploySdl); + const [sdlBuilderSdl, setSdlBuilderSdl] = useAtom(sdlStore.sdlBuilderSdl); const { data: providerAttributesSchema } = useProviderAttributesSchema(); const { enqueueSnackbar } = useSnackbar(); const { @@ -63,9 +66,16 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { const { services: _services } = watch(); const router = useRouter(); + useEffect(() => { + if (sdlBuilderSdl && sdlBuilderSdl.services) { + setValue("services", sdlBuilderSdl.services); + } + }, []); + // Load the template from query string on mount useEffect(() => { if ((router.query.id && !templateMetadata) || (router.query.id && templateMetadata?.id !== router.query.id)) { + // Load user template loadTemplate(router.query.id as string); } else if (!router.query.id && templateMetadata) { setTemplateMetadata(null); @@ -73,6 +83,12 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { } }, [router.query.id, templateMetadata]); + useEffect(() => { + if (_services) { + setSdlBuilderSdl({ services: _services }); + } + }, [_services]); + const loadTemplate = async (id: string) => { try { setIsLoadingTemplate(true); @@ -110,8 +126,6 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { try { const sdl = generateSdl(data); - setSdlResult(sdl); - setDeploySdl({ title: "", category: "", @@ -139,6 +153,23 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { } }; + const onPreviewSdlClick = () => { + setError(null); + + try { + const sdl = generateSdl({ services: _services }); + setSdlResult(sdl); + setIsPreviewingSdl(true); + + event(AnalyticsEvents.PREVIEW_SDL, { + category: "sdl_builder", + label: "Preview SDL from create page" + }); + } catch (error) { + setError(error.message); + } + }; + const getTemplateData = () => { const sdl = generateSdl({ services: _services }); const template: Partial = { @@ -169,6 +200,7 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { return ( <> {isImportingSdl && setIsImportingSdl(false)} setValue={setValue} />} + {isPreviewingSdl && setIsPreviewingSdl(false)} sdl={sdlResult} />} {isSavingTemplate && ( setIsSavingTemplate(false)} @@ -222,6 +254,10 @@ export const SimpleSDLBuilderForm: React.FunctionComponent = ({}) => { Deploy + + diff --git a/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx b/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx index 683ed45ac..51b217195 100644 --- a/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx +++ b/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx @@ -103,7 +103,9 @@ export const CertificateProvider = ({ children }) => { console.log(error); setIsLoadingCertificates(false); - enqueueSnackbar(, { variant: "error" }); + if (showSnackbar) { + enqueueSnackbar(, { variant: "error" }); + } return []; } diff --git a/deploy-web/src/pages/deployments/index.tsx b/deploy-web/src/pages/deployments/index.tsx index ce9ea1ef6..0f9da68ba 100644 --- a/deploy-web/src/pages/deployments/index.tsx +++ b/deploy-web/src/pages/deployments/index.tsx @@ -79,7 +79,7 @@ const DeploymentsPage: React.FunctionComponent = ({}) => { const end = start + rowsPerPage; const currentPageDeployments = orderedDeployments.slice(start, end); const pageCount = Math.ceil(orderedDeployments.length / rowsPerPage); - const [deploySdl, setDeploySdl] = useAtom(sdlStore.deploySdl); + const [, setDeploySdl] = useAtom(sdlStore.deploySdl); useEffect(() => { if (isWalletLoaded && isSettingsInit) { diff --git a/deploy-web/src/pages/new-deployment/index.tsx b/deploy-web/src/pages/new-deployment/index.tsx index a75c1e7b3..2f9122583 100644 --- a/deploy-web/src/pages/new-deployment/index.tsx +++ b/deploy-web/src/pages/new-deployment/index.tsx @@ -38,10 +38,10 @@ const NewDeploymentPage: React.FunctionComponent = ({}) => { const { isLoading: isLoadingTemplates, templates } = useTemplates(); const [activeStep, setActiveStep] = useState(0); const [selectedTemplate, setSelectedTemplate] = useState(null); + const deploySdl = useAtomValue(sdlStore.deploySdl); const [editedManifest, setEditedManifest] = useState(null); const { getDeploymentData } = useLocalNotes(); const { getTemplateById } = useTemplates(); - const deploySdl = useAtomValue(sdlStore.deploySdl); const router = useRouter(); const previousRoute = usePreviousRoute(); @@ -54,9 +54,11 @@ const NewDeploymentPage: React.FunctionComponent = ({}) => { if (redeployTemplate) { // If it's a redeploy, set the template from local storage setSelectedTemplate(redeployTemplate); + setEditedManifest(redeployTemplate.content); } else if (galleryTemplate) { // If it's a deploy from the template gallery, load from template data setSelectedTemplate(galleryTemplate as TemplateCreation); + setEditedManifest(galleryTemplate.content); } const _activeStep = getStepIndexByParam(router.query.step); @@ -68,10 +70,6 @@ const NewDeploymentPage: React.FunctionComponent = ({}) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.query, templates]); - useEffect(() => { - setEditedManifest(selectedTemplate?.content); - }, [selectedTemplate]); - const getRedeployTemplate = () => { let template = null; if (router.query.redeploy) { @@ -148,7 +146,7 @@ const NewDeploymentPage: React.FunctionComponent = ({}) => { - {activeStep === 0 && setSelectedTemplate(c)} />} + {activeStep === 0 && } {activeStep === 1 && } {activeStep === 2 && } diff --git a/deploy-web/src/store/sdlStore.ts b/deploy-web/src/store/sdlStore.ts index 1f26b8c19..9f1c68bb5 100644 --- a/deploy-web/src/store/sdlStore.ts +++ b/deploy-web/src/store/sdlStore.ts @@ -1,8 +1,12 @@ -import { TemplateCreation } from "@src/types"; +import { SdlBuilderFormValues, TemplateCreation } from "@src/types"; import { atom } from "jotai"; const deploySdl = atom(null as TemplateCreation); +const sdlBuilderSdl = atom(null as SdlBuilderFormValues); +const selectedSdlEditMode = atom<"yaml" | "builder">("yaml"); export default { - deploySdl + deploySdl, + sdlBuilderSdl, + selectedSdlEditMode }; diff --git a/deploy-web/src/utils/analytics.ts b/deploy-web/src/utils/analytics.ts index 8fa323afb..3eee5e96c 100644 --- a/deploy-web/src/utils/analytics.ts +++ b/deploy-web/src/utils/analytics.ts @@ -22,6 +22,7 @@ export enum AnalyticsEvents { // SDL Builder DEPLOY_SDL = "deploy_sdl", + PREVIEW_SDL = "preview_sdl", IMPORT_SDL = "import_sdl", RESET_SDL = "reset_sdl", CREATE_SDL_TEMPLATE = "create_sdl_template",