diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index c9a158f0c..b6913c22d 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -372,6 +372,7 @@ export enum TerminalKind { Default = 0, Mongo = 1, Cassandra = 2, + Postgres = 3, } export interface DataExplorerInputsFrame { diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx index 9df968226..3fb8c7d2e 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx @@ -74,6 +74,8 @@ export class NotebookTerminalComponent extends React.Component this.refreshCommandBarButtons(), (state) => state.isNotebooksEnabledForAccount @@ -353,7 +354,7 @@ export default class Explorer { (notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined)) ) { const provisionData: IProvisionData = { - cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, + cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint, poolId: PoolIdType.DefaultPoolId, }; const connectionStatus: ContainerConnectionInfo = { @@ -1058,6 +1059,10 @@ export default class Explorer { title = "Cassandra Shell"; break; + case ViewModels.TerminalKind.Postgres: + title = "PSQL Shell"; + break; + default: throw new Error("Terminal kind: ${kind} not supported"); } diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index f1332a8ff..5f77b8030 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -85,11 +85,14 @@ export function createStaticCommandBarButtons( (userContext.apiType === "Mongo" && useNotebook.getState().isShellEnabled && selectedNodeState.isDatabaseNodeOrNoneSelected()) || - userContext.apiType === "Cassandra" + userContext.apiType === "Cassandra" || + userContext.apiType === "Postgres" ) { notebookButtons.push(createDivider()); if (userContext.apiType === "Cassandra") { notebookButtons.push(createOpenCassandraTerminalButton(container)); + } else if (userContext.apiType === "Postgres") { + notebookButtons.push(createOpenPsqlTerminalButton(container)); } else { notebookButtons.push(createOpenMongoTerminalButton(container)); } @@ -523,6 +526,28 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo }; } +function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps { + const label = "Open PSQL Shell"; + const disableButton = + !useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled; + return { + iconSrc: HostedTerminalIcon, + iconAlt: label, + onCommandClick: () => { + if (useNotebook.getState().isNotebookEnabled) { + container.openNotebookTerminal(ViewModels.TerminalKind.Postgres); + } + }, + commandButtonLabel: label, + hasPopup: false, + disabled: disableButton, + ariaLabel: label, + tooltipText: !disableButton + ? "" + : "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.", + }; +} + function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps { const label = "Reset Workspace"; return { diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 030b05a88..bf796a536 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -23,7 +23,7 @@ export class NotebookContainerClient { private scheduleTimerId: NodeJS.Timeout; constructor(private onConnectionLost: () => void) { - this.phoenixClient = new PhoenixClient(); + this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); this.retryOptions = { retries: Notebook.retryAttempts, maxTimeout: Notebook.retryAttemptDelayMs, diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index b32d60eaf..bca244a90 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -307,7 +307,7 @@ export const useNotebook: UseStore = create((set, get) => ({ let isPhoenixFeatures = false; const isPublicInternetAllowed = isPublicInternetAccessAllowed(); - const phoenixClient = new PhoenixClient(); + const phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus(); if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) { diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index a66023041..4a3a8942e 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -24,6 +24,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], "phoenixClient": PhoenixClient { + "armResourceId": undefined, "retryOptions": Object { "maxTimeout": 5000, "minTimeout": 5000, diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index e77d8c74d..8447f3b03 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -14,6 +14,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "onRefreshDatabasesKeyPress": [Function], "onRefreshResourcesClick": [Function], "phoenixClient": PhoenixClient { + "armResourceId": undefined, "retryOptions": Object { "maxTimeout": 5000, "minTimeout": 5000, diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx index 1c010d6b3..2b2761d06 100644 --- a/src/Explorer/Tabs/TerminalTab.tsx +++ b/src/Explorer/Tabs/TerminalTab.tsx @@ -95,6 +95,10 @@ export default class TerminalTab extends TabsBase { endpointSuffix = "cassandra"; break; + case ViewModels.TerminalKind.Postgres: + endpointSuffix = "postgresql"; + break; + default: throw new Error(`Terminal kind: ${options.kind} not supported`); } diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts index 21d7ac612..aea03f8aa 100644 --- a/src/Phoenix/PhoenixClient.ts +++ b/src/Phoenix/PhoenixClient.ts @@ -32,6 +32,7 @@ import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; export class PhoenixClient { + private armResourceId: string; private containerHealthHandler: NodeJS.Timeout; private retryOptions: promiseRetry.Options = { retries: Notebook.retryAttempts, @@ -39,6 +40,10 @@ export class PhoenixClient { minTimeout: Notebook.retryAttemptDelayMs, }; + constructor(armResourceId: string) { + this.armResourceId = armResourceId; + } + public async allocateContainer(provisionData: IProvisionData): Promise> { return this.executeContainerAssignmentOperation(provisionData, "allocate"); } @@ -214,22 +219,21 @@ export class PhoenixClient { } } - public static getPhoenixEndpoint(): string { - const phoenixEndpoint = + private getPhoenixControlPlanePathPrefix(): string { + if (!this.armResourceId) { + throw new Error("The Phoenix client was not initialized properly: missing ARM resourcce id"); + } + + const toolsEndpoint = userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; - if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) { - const error = `${phoenixEndpoint} not allowed as juno endpoint`; + + if (!validateEndpoint(toolsEndpoint, allowedJunoOrigins)) { + const error = `${toolsEndpoint} not allowed as tools endpoint`; console.error(error); throw new Error(error); } - return phoenixEndpoint; - } - - public getPhoenixControlPlanePathPrefix(): string { - return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts${ - userContext.databaseAccount.id - }`; + return `${toolsEndpoint}/api/controlplane/toolscontainer/cosmosaccounts${this.armResourceId}`; } private static getHeaders(): HeadersInit { diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts index d4edb9c0e..63ce367d2 100644 --- a/src/Terminal/JupyterLabAppFactory.ts +++ b/src/Terminal/JupyterLabAppFactory.ts @@ -24,6 +24,10 @@ export class JupyterLabAppFactory { this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh"); } + private isPostgresShellStarted(content: string | undefined) { + this.isShellStarted = content?.includes("citus=>"); + } + constructor(closeTab: () => void) { this.onShellExited = closeTab; this.isShellStarted = false; @@ -36,6 +40,9 @@ export class JupyterLabAppFactory { case "Cassandra": this.checkShellStarted = this.isCassandraShellStarted; break; + case "Postgres": + this.checkShellStarted = this.isPostgresShellStarted; + break; } }