diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index b6913c22d..fbfbec69c 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -397,6 +397,8 @@ export interface DataExplorerInputsFrame { dataExplorerVersion?: string; defaultCollectionThroughput?: CollectionCreationDefaults; isPostgresAccount?: boolean; + // TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array + connectionStringParams?: any; flights?: readonly string[]; features?: { [key: string]: string; diff --git a/src/Explorer/Tabs/PostgresConnectTab.tsx b/src/Explorer/Tabs/PostgresConnectTab.tsx new file mode 100644 index 000000000..3487f7ee3 --- /dev/null +++ b/src/Explorer/Tabs/PostgresConnectTab.tsx @@ -0,0 +1,133 @@ +import { + Checkbox, + Dropdown, + Icon, + IconButton, + IDropdownOption, + ITextFieldStyles, + Label, + Link, + Stack, + Text, + TextField, + TooltipHost, +} from "@fluentui/react"; +import React from "react"; +import { userContext } from "UserContext"; + +export const PostgresConnectTab: React.FC = (): JSX.Element => { + const { adminLogin, nodes, enablePublicIpAccess } = userContext.postgresConnectionStrParams; + const [usePgBouncerPort, setUsePgBouncerPort] = React.useState(false); + const [selectedNode, setSelectedNode] = React.useState(nodes?.[0]?.value); + const portNumber = usePgBouncerPort ? "6432" : "5432"; + + const onCopyBtnClicked = (selector: string): void => { + const textfield: HTMLInputElement = document.querySelector(selector); + textfield.select(); + document.execCommand("copy"); + }; + + const textfieldStyles: Partial = { + root: { width: "100%" }, + field: { backgroundColor: "rgb(230, 230, 230)" }, + fieldGroup: { borderColor: "rgb(138, 136, 134)" }, + subComponentStyles: { label: { fontWeight: 400 } }, + description: { fontWeight: 400 }, + }; + + const nodesDropdownOptions: IDropdownOption[] = nodes.map((node) => ({ + key: node.value, + text: node.text, + })); + + const postgresSQLConnectionURL = `postgres://${adminLogin}:{your_password}@${selectedNode}:${portNumber}/citus?sslmode=require`; + const psql = `psql "host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require"`; + const jdbc = `jdbc:postgresql://${selectedNode}:${portNumber}/citus?user=${adminLogin}&password={your_password}&sslmode=require`; + const libpq = `host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require`; + const adoDotNet = `Server=${selectedNode};Database=citus;Port=${portNumber};User Id=${adminLogin};Password={your_password};Ssl Mode=Require;`; + + return ( +
+ + + + + + + + + + + { + const selectedNode = option.key as string; + setSelectedNode(selectedNode); + if (!selectedNode.startsWith("c.")) { + setUsePgBouncerPort(false); + } + }} + style={{ width: 200 }} + /> + + + + setUsePgBouncerPort(checked)} + disabled={!selectedNode?.startsWith("c.")} + /> + + + + onCopyBtnClicked("#postgresSQLConnectionURL")} /> + + + + + onCopyBtnClicked("#psql")} /> + + + + + onCopyBtnClicked("#JDBC")} /> + + + + + onCopyBtnClicked("#libpq")} /> + + + + onCopyBtnClicked("#adoDotNet")} /> + + + + + Only secure connections are supported. For production use cases, we recommend using the 'verify-full' + mode to enforce TLS certificate verification. You will need to download the Hyperscale (Citus) certificate, and + provide it when connecting to the database.{" "} + + Learn more + + +
+ ); +}; diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 7bdb0c68c..c64b6ed56 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -2,10 +2,12 @@ import { CollectionTabKind } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { ConnectTab } from "Explorer/Tabs/ConnectTab"; +import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import ko from "knockout"; import React, { MutableRefObject, useEffect, useRef, useState } from "react"; +import { userContext } from "UserContext"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import errorIcon from "../../../images/close-black.svg"; import { useObservable } from "../../hooks/useObservable"; @@ -189,7 +191,7 @@ const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { switch (activeReactTab) { case ReactTabKind.Connect: - return ; + return userContext.apiType === "Postgres" ? : ; case ReactTabKind.Home: return ; case ReactTabKind.Quickstart: diff --git a/src/UserContext.ts b/src/UserContext.ts index 4fcb19295..29548e3f9 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -26,6 +26,20 @@ export interface CollectionCreationDefaults { throughput: ThroughputDefaults; } +export interface Node { + text: string; + value: string; + ariaLabel: string; +} + +export interface PostgresConnectionStrParams { + adminLogin: string; + enablePublicIpAccess: boolean; + nodes: Node[]; + isMarlinServerGroup: boolean; + isFreeTier: boolean; +} + interface UserContext { readonly authType?: AuthType; readonly masterKey?: string; @@ -52,6 +66,7 @@ interface UserContext { collectionId: string; partitionKey?: string; }; + readonly postgresConnectionStrParams?: PostgresConnectionStrParams; collectionCreationDefaults: CollectionCreationDefaults; } diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index e93d28196..1a26813d8 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -28,7 +28,7 @@ import { } from "../Platform/Hosted/HostedUtils"; import { CollectionCreation } from "../Shared/Constants"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; -import { PortalEnv, updateUserContext, userContext } from "../UserContext"; +import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; import { getMsalInstance } from "../Utils/AuthorizationUtils"; @@ -357,6 +357,20 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { if (inputs.isPostgresAccount) { updateUserContext({ apiType: "Postgres" }); + + if (inputs.connectionStringParams) { + // TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension + let flattenedNodesArray: Node[] = []; + inputs.connectionStringParams.nodes?.forEach((node: Node | Node[]) => { + if (Array.isArray(node)) { + flattenedNodesArray = [...flattenedNodesArray, ...node]; + } else { + flattenedNodesArray.push(node); + } + }); + inputs.connectionStringParams.nodes = flattenedNodesArray; + updateUserContext({ postgresConnectionStrParams: inputs.connectionStringParams }); + } } if (inputs.features) {