diff --git a/dashboard/src/components/porter/BlockSelect.tsx b/dashboard/src/components/porter/BlockSelect.tsx index d48bb78cdf..0f1934ba5e 100644 --- a/dashboard/src/components/porter/BlockSelect.tsx +++ b/dashboard/src/components/porter/BlockSelect.tsx @@ -100,6 +100,7 @@ const Block = styled.div<{ selected?: boolean; disabled?: boolean }>` display: flex; flex-direction: column; height: 100%; + width: 100%; align-items: left; user-select: none; font-size: 13px; diff --git a/dashboard/src/main/home/database-dashboard/forms/CreateDatastore.tsx b/dashboard/src/main/home/database-dashboard/forms/CreateDatastore.tsx index cac2975d0d..3fe77804f7 100644 --- a/dashboard/src/main/home/database-dashboard/forms/CreateDatastore.tsx +++ b/dashboard/src/main/home/database-dashboard/forms/CreateDatastore.tsx @@ -1,12 +1,26 @@ -import React from "react"; +import React, { useContext } from "react"; +import { match } from "ts-pattern"; + +import Loading from "components/Loading"; + +import { Context } from "shared/Context"; import DatastoreFormContextProvider from "../DatastoreFormContextProvider"; import DatastoreForm from "./DatastoreForm"; +import SandboxDatastoreForm from "./SandboxDatastoreForm"; const CreateDatastore: React.FC = () => { + const { currentProject } = useContext(Context); + + if (!currentProject) { + return ; + } return ( - + {match(currentProject) + .with({ sandbox_enabled: true }, () => ) + .with({ sandbox_enabled: false }, () => ) + .exhaustive()} ); }; diff --git a/dashboard/src/main/home/database-dashboard/forms/DatastoreForm.tsx b/dashboard/src/main/home/database-dashboard/forms/DatastoreForm.tsx index 00ac687820..658e583419 100644 --- a/dashboard/src/main/home/database-dashboard/forms/DatastoreForm.tsx +++ b/dashboard/src/main/home/database-dashboard/forms/DatastoreForm.tsx @@ -35,7 +35,6 @@ import { DATASTORE_TEMPLATE_AWS_RDS, DATASTORE_TEMPLATE_MANAGED_POSTGRES, DATASTORE_TEMPLATE_MANAGED_REDIS, - DATASTORE_TEMPLATE_NEON, SUPPORTED_DATASTORE_TEMPLATES, } from "../constants"; import { useDatastoreFormContext } from "../DatastoreFormContextProvider"; @@ -74,7 +73,8 @@ const DatastoreForm: React.FC = () => { const availableEngines: BlockSelectOption[] = useMemo(() => { return [DATASTORE_ENGINE_POSTGRES, DATASTORE_ENGINE_REDIS]; - }, [awsClusters, watchClusterId]); + }, [watchClusterId]); + const availableWorkloadTypes: BlockSelectOption[] = useMemo(() => { return [ { @@ -106,7 +106,6 @@ const DatastoreForm: React.FC = () => { DATASTORE_TEMPLATE_AWS_RDS, DATASTORE_TEMPLATE_AWS_AURORA, DATASTORE_TEMPLATE_AWS_ELASTICACHE, - DATASTORE_TEMPLATE_NEON, ] : [ DATASTORE_TEMPLATE_MANAGED_POSTGRES, @@ -289,14 +288,6 @@ const DatastoreForm: React.FC = () => { setValue("config.masterUsername", "postgres"); setValue("config.engineVersion", "15.4"); } - ) - .with( - { - name: DATASTORE_TEMPLATE_NEON.name, - }, - () => { - setValue("config.type", "neon"); - } ); setValue("config.instanceClass", "unspecified"); setValue("config.masterUserPassword", uuidv4()); @@ -326,78 +317,75 @@ const DatastoreForm: React.FC = () => { )} , - template !== DATASTORE_TEMPLATE_NEON ? ( - <> - Specify resources - {template && ( - <> - - - Specify your datastore CPU and RAM. - - {errors.config?.instanceClass?.message && ( - - - - - )} - - Select an instance tier: - - { - setValue("config.instanceClass", option.tier); - setValue( - "config.allocatedStorageGigabytes", - option.storageGigabytes - ); - setCurrentStep(6); - }} - highlight={watchEngine === "REDIS" ? "ram" : "storage"} - /> - - )} - - ) : null, - template !== DATASTORE_TEMPLATE_NEON ? ( - <> - Credentials - {watchInstanceClass !== "unspecified" && template && ( - <> - - - These credentials never leave your own cloud environment. - Your app will use them to connect to this datastore. - - - - - )} - - ) : null, + + <> + Specify resources + {template && ( + <> + + + Specify your datastore CPU and RAM. + + {errors.config?.instanceClass?.message && ( + + + + + )} + + Select an instance tier: + + { + setValue("config.instanceClass", option.tier); + setValue( + "config.allocatedStorageGigabytes", + option.storageGigabytes + ); + setCurrentStep(6); + }} + highlight={watchEngine === "REDIS" ? "ram" : "storage"} + /> + + )} + , + <> + Credentials + {watchInstanceClass !== "unspecified" && template && ( + <> + + + These credentials never leave your own cloud environment. + Your app will use them to connect to this datastore. + + + + + )} + , <> Create datastore instance diff --git a/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx b/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx new file mode 100644 index 0000000000..70590cf62d --- /dev/null +++ b/dashboard/src/main/home/database-dashboard/forms/SandboxDatastoreForm.tsx @@ -0,0 +1,236 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import styled from "styled-components"; +import { match } from "ts-pattern"; + +import Back from "components/porter/Back"; +import BlockSelect, { + type BlockSelectOption, +} from "components/porter/BlockSelect"; +import Button from "components/porter/Button"; +import { ControlledInput } from "components/porter/ControlledInput"; +import Selector from "components/porter/Selector"; +import Spacer from "components/porter/Spacer"; +import Text from "components/porter/Text"; +import VerticalSteps from "components/porter/VerticalSteps"; +import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader"; +import { type DatastoreTemplate, type DbFormData } from "lib/databases/types"; +import { useClusterList } from "lib/hooks/useCluster"; + +import { valueExists } from "shared/util"; +import database from "assets/database.svg"; + +import { + DATASTORE_ENGINE_POSTGRES, + DATASTORE_ENGINE_REDIS, + DATASTORE_TEMPLATE_NEON, + SUPPORTED_DATASTORE_TEMPLATES, +} from "../constants"; +import { useDatastoreFormContext } from "../DatastoreFormContextProvider"; + +const SandboxDatastoreForm: React.FC = () => { + const [currentStep, setCurrentStep] = useState(0); + const [template, setTemplate] = useState( + undefined + ); + + const { clusters } = useClusterList(); + + const { + setValue, + formState: { errors }, + register, + watch, + } = useFormContext(); + const watchClusterId = watch("clusterId", 0); + const watchEngine = watch("engine", "UNKNOWN"); + + const { updateDatastoreButtonProps } = useDatastoreFormContext(); + + const availableEngines: BlockSelectOption[] = useMemo(() => { + return [ + DATASTORE_ENGINE_POSTGRES, + { + ...DATASTORE_ENGINE_REDIS, + disabledOpts: { + tooltipText: "Coming soon!", + }, + }, + ]; + }, [watchClusterId]); + + const availableHostTypes: BlockSelectOption[] = useMemo(() => { + const options = [DATASTORE_TEMPLATE_NEON].filter( + (t) => t.highLevelType.name === watchEngine + ); + return options; + }, [watchEngine]); + + useEffect(() => { + if (clusters.length > 0) { + setValue("clusterId", clusters[0].id); + } + }, [JSON.stringify(clusters)]); + + return ( +
+ + + } + title={"Create a new datastore"} + capitalize={false} + disableLineBreak + /> + + + Datastore type + + ( + e.name === value + )} + setOption={(opt) => { + onChange(opt.name); + setValue("workloadType", "unspecified"); + setTemplate(undefined); + setCurrentStep(1); + }} + /> + )} + /> + , + <> + Datastore name + {watchEngine !== "UNKNOWN" && ( + <> + + + Lowercase letters, numbers, and "-" only. + + + { + setValue("name", e.target.value); + setCurrentStep(Math.max(2, currentStep)); + }} + /> + {clusters.length > 1 && ( + <> + + + activeValue={watchClusterId.toString()} + width="300px" + options={clusters.map((c) => ({ + value: c.id.toString(), + label: c.vanity_name, + key: c.id.toString(), + }))} + setActiveValue={(value: string) => { + setValue("clusterId", parseInt(value)); + setValue("workloadType", "unspecified"); + setCurrentStep(2); + }} + label={"Cluster"} + /> + + )} + + )} + , + <> + Hosting option + {currentStep >= 2 && ( + <> + + a.name === template?.name + )} + setOption={(opt) => { + const templateMatch = SUPPORTED_DATASTORE_TEMPLATES.find( + (t) => t.name === opt.name + ); + if (!templateMatch) { + return; + } + setTemplate(templateMatch); + match(templateMatch).with( + { + name: DATASTORE_TEMPLATE_NEON.name, + }, + () => { + setValue("config.type", "neon"); + } + ); + setCurrentStep(4); + }} + /> + + )} + , + <> + Create datastore instance + + + , + ].filter(valueExists)} + currentStep={currentStep} + /> + +
+ ); +}; + +export default SandboxDatastoreForm; + +const Div = styled.div` + width: 100%; + max-width: 900px; +`; + +const StyledConfigureTemplate = styled.div` + height: 100%; +`; + +const DarkMatter = styled.div` + width: 100%; + margin-top: -5px; +`; + +const Icon = styled.img` + margin-right: 15px; + height: 30px; + animation: floatIn 0.5s; + animation-fill-mode: forwards; + + @keyframes floatIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0px); + } + } +`; diff --git a/dashboard/src/main/home/database-dashboard/tabs/PublicDatastoreConnectTab.tsx b/dashboard/src/main/home/database-dashboard/tabs/PublicDatastoreConnectTab.tsx index c3fc68f828..2c301b5b43 100644 --- a/dashboard/src/main/home/database-dashboard/tabs/PublicDatastoreConnectTab.tsx +++ b/dashboard/src/main/home/database-dashboard/tabs/PublicDatastoreConnectTab.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React from "react"; import styled from "styled-components"; import Banner from "components/porter/Banner"; @@ -7,17 +7,12 @@ import ShowIntercomButton from "components/porter/ShowIntercomButton"; import Spacer from "components/porter/Spacer"; import Text from "components/porter/Text"; -import { Context } from "shared/Context"; - import { useDatastoreContext } from "../DatabaseContextProvider"; -import ConnectAppsModal from "../shared/ConnectAppsModal"; import ConnectionInfo from "../shared/ConnectionInfo"; // use this for external datastores that are publicly exposed like neon, upstash, etc. const PublicDatastoreConnectTab: React.FC = () => { const { datastore } = useDatastoreContext(); - const { currentProject } = useContext(Context); - const [showConnectAppsModal, setShowConnectAppsModal] = useState(false); if (datastore.credential.host === "") { return ( @@ -60,26 +55,6 @@ const PublicDatastoreConnectTab: React.FC = () => { The datastore client of your application should use these credentials to create a connection.{" "} - {!currentProject?.sandbox_enabled && ( - <> - - { - setShowConnectAppsModal(true); - }} - > - add - Inject these credentials into an app - - {showConnectAppsModal && ( - { - setShowConnectAppsModal(false); - }} - /> - )} - - )} ); @@ -93,34 +68,3 @@ const ConnectTabContainer = styled.div` display: flex; flex-direction: row; `; - -const ConnectAppButton = styled.div` - color: #aaaabb; - background: ${({ theme }) => theme.fg}; - border: 1px solid #494b4f; - :hover { - border: 1px solid #7a7b80; - color: white; - } - display: flex; - align-items: center; - border-radius: 5px; - height: 40px; - font-size: 13px; - width: 100%; - padding-left: 10px; - cursor: pointer; - .add-icon { - width: 30px; - font-size: 20px; - } -`; - -const I = styled.i` - color: white; - font-size: 14px; - display: flex; - align-items: center; - margin-right: 7px; - justify-content: center; -`;