();
+ 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/shared/ConnectionInfo.tsx b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx
index efcf2e68eb..750da78703 100644
--- a/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx
+++ b/dashboard/src/main/home/database-dashboard/shared/ConnectionInfo.tsx
@@ -1,5 +1,6 @@
-import React from "react";
+import React, { useMemo } from "react";
import styled from "styled-components";
+import { match } from "ts-pattern";
import ClickToCopy from "components/porter/ClickToCopy";
import Container from "components/porter/Container";
@@ -7,19 +8,62 @@ import Fieldset from "components/porter/Fieldset";
import Spacer from "components/porter/Spacer";
import Text from "components/porter/Text";
import {
+ DATASTORE_TYPE_ELASTICACHE,
+ DATASTORE_TYPE_MANAGED_POSTGRES,
+ DATASTORE_TYPE_MANAGED_REDIS,
+ DATASTORE_TYPE_NEON,
+ DATASTORE_TYPE_RDS,
type DatastoreConnectionInfo,
- type DatastoreEngine,
+ type DatastoreTemplate,
} from "lib/databases/types";
-import { DATASTORE_ENGINE_REDIS } from "../constants";
+import {
+ DATASTORE_ENGINE_POSTGRES,
+ DATASTORE_ENGINE_REDIS,
+} from "../constants";
type Props = {
connectionInfo: DatastoreConnectionInfo;
- engine: DatastoreEngine;
+ template: DatastoreTemplate;
};
-const ConnectionInfo: React.FC = ({ connectionInfo, engine }) => {
+const ConnectionInfo: React.FC = ({ connectionInfo, template }) => {
const [isPasswordHidden, setIsPasswordHidden] = React.useState(true);
+ const connectionString = useMemo(() => {
+ return match(template)
+ .returnType()
+ .with({ highLevelType: DATASTORE_ENGINE_REDIS }, () =>
+ match(template)
+ .with(
+ { type: DATASTORE_TYPE_ELASTICACHE },
+ () =>
+ `rediss://:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/0?ssl_cert_reqs=CERT_REQUIRED`
+ )
+ .with(
+ { type: DATASTORE_TYPE_MANAGED_REDIS },
+ () =>
+ `redis://:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/0`
+ )
+ .otherwise(() => "")
+ )
+ .with({ highLevelType: DATASTORE_ENGINE_POSTGRES }, () =>
+ match(template)
+ .with(
+ { type: DATASTORE_TYPE_RDS },
+ { type: DATASTORE_TYPE_NEON },
+ () =>
+ `postgres://${connectionInfo.username}:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/${connectionInfo.database_name}?sslmode=require`
+ )
+ .with(
+ { type: DATASTORE_TYPE_MANAGED_POSTGRES },
+ () =>
+ `postgres://${connectionInfo.username}:${connectionInfo.password}@${connectionInfo.host}:${connectionInfo.port}/${connectionInfo.database_name}`
+ )
+ .otherwise(() => "")
+ )
+ .otherwise(() => "");
+ }, [template]);
+
return (
diff --git a/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx b/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx
new file mode 100644
index 0000000000..3c0118efb8
--- /dev/null
+++ b/dashboard/src/main/home/database-dashboard/shared/NeonIntegrationModal.tsx
@@ -0,0 +1,60 @@
+import React, { useEffect } from "react";
+import { useQuery } from "@tanstack/react-query";
+
+import Link from "components/porter/Link";
+import Modal from "components/porter/Modal";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import { useAuthWindow } from "lib/hooks/useAuthWindow";
+import { useNeon } from "lib/hooks/useNeon";
+
+import { useDatastoreFormContext } from "../DatastoreFormContextProvider";
+
+type Props = {
+ onClose: () => void;
+};
+
+const NeonIntegrationModal: React.FC = ({ onClose }) => {
+ const { projectId } = useDatastoreFormContext();
+ const { getNeonIntegrations } = useNeon();
+ const { openAuthWindow } = useAuthWindow({
+ authUrl: `/api/projects/${projectId}/oauth/neon`,
+ });
+
+ const neonIntegrationsResp = useQuery(
+ ["getNeonIntegrations", projectId],
+ async () => {
+ const integrations = await getNeonIntegrations({
+ projectId,
+ });
+ return integrations;
+ },
+ {
+ enabled: !!projectId,
+ refetchInterval: 1000,
+ }
+ );
+ useEffect(() => {
+ if (
+ neonIntegrationsResp.isSuccess &&
+ neonIntegrationsResp.data.length > 0
+ ) {
+ onClose();
+ }
+ }, [neonIntegrationsResp]);
+
+ return (
+
+ Integrate Neon
+
+
+ To continue, you must authenticate with Neon.{" "}
+
+ Authorize Porter to create Neon datastores on your behalf
+
+
+
+ );
+};
+
+export default NeonIntegrationModal;
diff --git a/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx b/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx
index 81261cf853..2e223518e9 100644
--- a/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx
+++ b/dashboard/src/main/home/database-dashboard/tabs/ConnectTab.tsx
@@ -59,7 +59,7 @@ const ConnectTab: React.FC = () => {
@@ -124,14 +124,14 @@ const ConnectTab: React.FC = () => {
export default ConnectTab;
-const ConnectTabContainer = styled.div`
+export const ConnectTabContainer = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
`;
-const IdContainer = styled.div`
+export const IdContainer = styled.div`
width: fit-content;
background: #000000;
border-radius: 5px;
diff --git a/dashboard/src/main/home/database-dashboard/tabs/PublicDatastoreConnectTab.tsx b/dashboard/src/main/home/database-dashboard/tabs/PublicDatastoreConnectTab.tsx
new file mode 100644
index 0000000000..2c301b5b43
--- /dev/null
+++ b/dashboard/src/main/home/database-dashboard/tabs/PublicDatastoreConnectTab.tsx
@@ -0,0 +1,70 @@
+import React from "react";
+import styled from "styled-components";
+
+import Banner from "components/porter/Banner";
+import Container from "components/porter/Container";
+import ShowIntercomButton from "components/porter/ShowIntercomButton";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import { useDatastoreContext } from "../DatabaseContextProvider";
+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();
+
+ if (datastore.credential.host === "") {
+ return (
+
+
+ Talk to support
+
+ >
+ }
+ >
+ Error reaching your datastore for credentials. Please contact support.
+
+
+ );
+ }
+ return (
+
+
+
+ Connection info
+
+
+
+
+
+ The datastore client of your application should use these credentials
+ to create a connection.{" "}
+
+
+
+ );
+};
+
+export default PublicDatastoreConnectTab;
+
+const ConnectTabContainer = styled.div`
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+`;
diff --git a/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx b/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx
index 3ecbf390ab..ba10549d46 100644
--- a/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx
+++ b/dashboard/src/main/home/database-dashboard/tabs/SettingsTab.tsx
@@ -46,7 +46,6 @@ const SettingsTab: React.FC = () => {
{showDeleteDatastoreModal && (
{
setShowDeleteDatastoreModal(false);
}}
@@ -62,16 +61,16 @@ const SettingsTab: React.FC = () => {
export default SettingsTab;
type DeleteDatastoreModalProps = {
- datastoreName: string;
onSubmit: () => Promise;
onClose: () => void;
};
const DeleteDatastoreModal: React.FC = ({
- datastoreName,
onSubmit,
onClose,
}) => {
+ const { datastore } = useDatastoreContext();
+
const [inputtedDatastoreName, setInputtedDatastoreName] =
useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -114,33 +113,36 @@ const DeleteDatastoreModal: React.FC = ({
return (
- Delete {datastoreName}?
-
-
-
- Attention:
-
-
-
- Destruction of resources sometimes results in dangling resources. To
- ensure that everything has been properly destroyed, please visit your
- cloud provider's console.
-
-
-
- Deletion instructions
-
+ Delete {datastore.name}?
+ {datastore.cloud_provider_credential_identifier !== "" && (
+ <>
+
+ Attention:
+
+
+
+ Destruction of resources sometimes results in dangling resources. To
+ ensure that everything has been properly destroyed, please visit
+ your cloud provider's console.
+
+
+
+ Deletion instructions
+
+
+ >
+ )}
To confirm, enter the datastore name below. This action is irreversible.
= ({