diff --git a/images/firewallRule.png b/images/firewallRule.png new file mode 100644 index 000000000..fd388bd4d Binary files /dev/null and b/images/firewallRule.png differ diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index d128256c1..031a0686b 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -567,6 +567,16 @@ export interface ContainerConnectionInfo { //need to add ram and rom info } +export interface PostgresFirewallRule { + id: string; + name: string; + type: string; + properties: { + startIpAddress: string; + endIpAddress: string; + }; +} + export enum PhoenixErrorType { MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded", MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded", diff --git a/src/Explorer/Quickstart/QuickstartFirewallNotification.tsx b/src/Explorer/Quickstart/QuickstartFirewallNotification.tsx new file mode 100644 index 000000000..adba30082 --- /dev/null +++ b/src/Explorer/Quickstart/QuickstartFirewallNotification.tsx @@ -0,0 +1,21 @@ +import { Image, PrimaryButton, Stack, Text } from "@fluentui/react"; +import { sendMessage } from "Common/MessageHandler"; +import { MessageTypes } from "Contracts/ExplorerContracts"; +import React from "react"; + +export const QuickstartFirewallNotification: React.FC = (): JSX.Element => ( + + + To use the PostgreSQL shell, you need to add a firewall rule to allow access from all IP addresses + (0.0.0.0-255.255.255). + + We strongly recommend removing this rule once you finish using the PostgreSQL shell. + + sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })} + > + Add firewall rule + + +); diff --git a/src/Explorer/Quickstart/QuickstartGuide.tsx b/src/Explorer/Quickstart/QuickstartGuide.tsx index 46ed22d8b..9df39de0f 100644 --- a/src/Explorer/Quickstart/QuickstartGuide.tsx +++ b/src/Explorer/Quickstart/QuickstartGuide.tsx @@ -109,8 +109,12 @@ export const QuickstartGuide: React.FC = (): JSX.Element => {

To begin, please enter the cluster's password in the PostgreSQL terminal. +
+
+ Note: If you navigate out of the Quick Start tab (PostgreSQL Shell), the session will be closed and + all ongoing commands might be interrupted. - + { onClick={() => setCurrentStep(1)} > - Let’s create two tables github_users and github_events in “cosmosdb_tutorial” schema. + Let's create two tables github_users and github_events in “cosmosdb_tutorial” schema. useTerminal.getState().sendMessage(newTableCommand)} @@ -150,7 +154,7 @@ export const QuickstartGuide: React.FC = (): JSX.Element => { onClick={() => onCopyBtnClicked("#newTableCommand")} /> - + { onClick={() => onCopyBtnClicked("#distributeTableCommand")} /> - + { onClick={() => onCopyBtnClicked("#loadDataCommand")} /> - + { onClick={() => onCopyBtnClicked("#queryCommand")} /> - + diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index d6ac666d9..10882f8e8 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -347,10 +347,10 @@ export class SplashScreen extends React.Component { const connectBtn = { iconSrc: ConnectIcon, - title: userContext.apiType === "Postgres" ? "Connect with PG Admin" : "Connect", + title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect", description: userContext.apiType === "Postgres" - ? "Prefer PGadmin? Find your connection strings here" + ? "Prefer pgAdmin? Find your connection strings here" : "Prefer using your own choice of tooling? Find the connection string you need to connect", onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect), }; diff --git a/src/Explorer/Tabs/PostgresConnectTab.tsx b/src/Explorer/Tabs/PostgresConnectTab.tsx index 3487f7ee3..732b1d93f 100644 --- a/src/Explorer/Tabs/PostgresConnectTab.tsx +++ b/src/Explorer/Tabs/PostgresConnectTab.tsx @@ -120,7 +120,7 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking - + 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.{" "} @@ -128,6 +128,18 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking Learn more + + + + Refer to our{" "} + + guide + {" "} + to help you connect via pgAdmin. + ); }; diff --git a/src/Explorer/Tabs/QuickstartTab.tsx b/src/Explorer/Tabs/QuickstartTab.tsx index 12c94b8e3..5a3aac58c 100644 --- a/src/Explorer/Tabs/QuickstartTab.tsx +++ b/src/Explorer/Tabs/QuickstartTab.tsx @@ -1,11 +1,15 @@ import { Spinner, SpinnerSize, Stack } from "@fluentui/react"; -import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels"; +import { configContext } from "ConfigContext"; +import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels"; import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent"; import Explorer from "Explorer/Explorer"; import { useNotebook } from "Explorer/Notebook/useNotebook"; +import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification"; import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide"; -import React, { useEffect } from "react"; +import { ReactTabKind, useTabs } from "hooks/useTabs"; +import React, { useEffect, useState } from "react"; import { userContext } from "UserContext"; +import { armRequest } from "Utils/arm/request"; interface QuickstartTabProps { explorer: Explorer; @@ -13,29 +17,58 @@ interface QuickstartTabProps { export const QuickstartTab: React.FC = ({ explorer }: QuickstartTabProps): JSX.Element => { const notebookServerInfo = useNotebook((state) => state.notebookServerInfo); - useEffect(() => { - explorer.allocateContainer(); - }, []); + const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState(true); + const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({ authToken: notebookServerInfo.authToken, notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`, forwardingId: notebookServerInfo.forwardingId, }); + const checkFirewallRules = async (): Promise => { + const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response: any = await armRequest({ + host: configContext.ARM_ENDPOINT, + path: firewallRulesUri, + method: "GET", + apiVersion: "2020-10-05-privatepreview", + }); + const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || []; + const isEnabled = firewallRules.some( + (rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255" + ); + setIsAllPublicIPAddressEnabled(isEnabled); + + // If the firewall rule is not added, check every 30 seconds to see if the user has added the rule + if (!isEnabled && useTabs.getState().activeReactTab === ReactTabKind.Quickstart) { + setTimeout(checkFirewallRules, 30000); + } + }; + + useEffect(() => { + checkFirewallRules(); + }); + + useEffect(() => { + explorer.allocateContainer(); + }, []); + return ( - {notebookServerInfo?.notebookServerEndpoint && ( + {!isAllPublicIPAddressEnabled && } + {isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && ( )} - {!notebookServerInfo?.notebookServerEndpoint && ( + {isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && ( )} diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 2b2761d06..708352073 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -1,6 +1,9 @@ import { Spinner, SpinnerSize } from "@fluentui/react"; +import { configContext } from "ConfigContext"; +import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification"; import * as ko from "knockout"; import * as React from "react"; +import { armRequest } from "Utils/arm/request"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -26,10 +29,15 @@ class NotebookTerminalComponentAdapter implements ReactAdapter { constructor( private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo, private getDatabaseAccount: () => DataModels.DatabaseAccount, - private getTabId: () => string + private getTabId: () => string, + private isAllPublicIPAddressesEnabled: ko.Observable ) {} public renderComponent(): JSX.Element { + if (!this.isAllPublicIPAddressesEnabled()) { + return ; + } + return this.parameters() ? ( ; constructor(options: TerminalTabOptions) { super(options); this.container = options.container; + this.isAllPublicIPAddressesEnabled = ko.observable(true); this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter( () => this.getNotebookServerInfo(options), () => userContext?.databaseAccount, - () => this.tabId + () => this.tabId, + this.isAllPublicIPAddressesEnabled ); this.notebookTerminalComponentAdapter.parameters = ko.computed(() => { if ( this.isTemplateReady() && useNotebook.getState().isNotebookEnabled && - useNotebook.getState().notebookServerInfo?.notebookServerEndpoint + useNotebook.getState().notebookServerInfo?.notebookServerEndpoint && + this.isAllPublicIPAddressesEnabled() ) { return true; } return false; }); + + if (options.kind === ViewModels.TerminalKind.Postgres) { + this.checkPostgresFirewallRules(); + } } public getContainer(): Explorer { @@ -110,4 +126,25 @@ export default class TerminalTab extends TabsBase { forwardingId: info.forwardingId, }; } + + private async checkPostgresFirewallRules(): Promise { + const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response: any = await armRequest({ + host: configContext.ARM_ENDPOINT, + path: firewallRulesUri, + method: "GET", + apiVersion: "2020-10-05-privatepreview", + }); + const firewallRules: DataModels.PostgresFirewallRule[] = response?.data?.value || response?.value || []; + const isEnabled = firewallRules.some( + (rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255" + ); + this.isAllPublicIPAddressesEnabled(isEnabled); + + // If the firewall rule is not added, check every 30 seconds to see if the user has added the rule + if (!isEnabled) { + setTimeout(() => this.checkPostgresFirewallRules(), 30000); + } + } }