diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index acf58baaf..d128256c1 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -33,6 +33,7 @@ export interface DatabaseAccountExtendedProperties { privateEndpointConnections?: unknown[]; capacity?: { totalThroughputLimit: number }; locations?: DatabaseAccountResponseLocation[]; + postgresqlEndpoint?: string; } export interface DatabaseAccountResponseLocation { diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx index 3fb8c7d2e..082979a23 100644 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx +++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx @@ -2,6 +2,7 @@ * Wrapper around Notebook server terminal */ +import { useTerminal } from "hooks/useTerminal"; import postRobot from "post-robot"; import * as React from "react"; import * as DataModels from "../../../Contracts/DataModels"; @@ -40,6 +41,7 @@ export class NotebookTerminalComponent extends React.Component): void { this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow; + useTerminal.getState().setTerminal(this.terminalWindow); this.sendPropsToTerminalFrame(); } @@ -75,7 +77,7 @@ export class NotebookTerminalComponent extends React.Component { @@ -1249,9 +1247,11 @@ export default class Explorer { } public async refreshExplorer(): Promise { - userContext.authType === AuthType.ResourceToken - ? this.refreshDatabaseForResourceToken() - : this.refreshAllDatabases(); + if (userContext.apiType !== "Postgres") { + userContext.authType === AuthType.ResourceToken + ? this.refreshDatabaseForResourceToken() + : this.refreshAllDatabases(); + } await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 5f77b8030..c02639f09 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -85,14 +85,11 @@ export function createStaticCommandBarButtons( (userContext.apiType === "Mongo" && useNotebook.getState().isShellEnabled && selectedNodeState.isDatabaseNodeOrNoneSelected()) || - userContext.apiType === "Cassandra" || - userContext.apiType === "Postgres" + userContext.apiType === "Cassandra" ) { 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)); } @@ -612,16 +609,7 @@ function createStaticCommandBarButtonsForResourceToken( } export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] { - const postgreShellLabel = "Open PostgreSQL Shell"; - const openPostgreShellBtn = { - iconSrc: HostedTerminalIcon, - iconAlt: postgreShellLabel, - onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Mongo), - commandButtonLabel: postgreShellLabel, - hasPopup: false, - disabled: false, - ariaLabel: postgreShellLabel, - }; + const openPostgreShellBtn = createOpenPsqlTerminalButton(container); return [openPostgreShellBtn]; } diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index bca244a90..4386c4da4 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -124,8 +124,9 @@ export const useNotebook: UseStore = create((set, get) => ({ } const firstWriteLocation = - databaseAccount?.properties?.writeLocations && - databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase(); + userContext.apiType === "Postgres" + ? databaseAccount?.location + : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase(); const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`; const authorizationHeader = getAuthorizationHeader(); try { @@ -313,7 +314,10 @@ export const useNotebook: UseStore = create((set, get) => ({ if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) { if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) { isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true; - isPhoenixFeatures = isPublicInternetAllowed && userContext.features.phoenixFeatures === true; + isPhoenixFeatures = + isPublicInternetAllowed && + // phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers + (userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres"); } else { isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed; } diff --git a/src/Explorer/Quickstart/PostgreQuickstartCommands.ts b/src/Explorer/Quickstart/PostgreQuickstartCommands.ts new file mode 100644 index 000000000..d70028885 --- /dev/null +++ b/src/Explorer/Quickstart/PostgreQuickstartCommands.ts @@ -0,0 +1,105 @@ +export const newTableCommand = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE; +CREATE SCHEMA cosmosdb_tutorial; +SET search_path to cosmosdb_tutorial; +CREATE TABLE github_users +( + user_id bigint, + url text, + login text, + avatar_url text, + gravatar_id text, + display_login text +); +CREATE TABLE github_events +( + event_id bigint, + event_type text, + event_public boolean, + repo_id bigint, + payload jsonb, + repo jsonb, + user_id bigint, + org jsonb, + created_at timestamp +); +CREATE INDEX event_type_index ON github_events (event_type); +CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops); +`; + +export const newTableCommandForDisplay = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE; +CREATE SCHEMA cosmosdb_tutorial; + +-- Using schema created for tutorial +SET search_path to cosmosdb_tutorial; + +CREATE TABLE github_users +( + user_id bigint, + url text, + login text, + avatar_url text, + gravatar_id text, + display_login text +); + +CREATE TABLE github_events +( + event_id bigint, + event_type text, + event_public boolean, + repo_id bigint, + payload jsonb, + repo jsonb, + user_id bigint, + org jsonb, + created_at timestamp +); + +--Create indexes on events table +CREATE INDEX event_type_index ON github_events (event_type); +CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);`; + +export const distributeTableCommand = `SET search_path to cosmosdb_tutorial; +SELECT create_distributed_table('github_users', 'user_id'); +SELECT create_distributed_table('github_events', 'user_id'); +`; + +export const distributeTableCommandForDisplay = `-- Using schema created for the tutorial +SET search_path to cosmosdb_tutorial; + +SELECT create_distributed_table('github_users', 'user_id'); +SELECT create_distributed_table('github_events', 'user_id');`; + +export const loadDataCommand = `SET search_path to cosmosdb_tutorial; +\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV); +\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV); +`; + +export const loadDataCommandForDisplay = `-- Using schema created for the tutorial +SET search_path to cosmosdb_tutorial; + +-- download users and store in table +\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV); +\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV);`; + +export const queryCommand = `SET search_path to cosmosdb_tutorial; +SELECT count(*) FROM github_users; +SELECT created_at, event_type, repo->>'name' AS repo_name +FROM github_events +WHERE user_id = 3861633; +SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour; +`; + +export const queryCommandForDisplay = `-- Using schema created for the tutorial +SET search_path to cosmosdb_tutorial; + +-- count all rows (across shards) +SELECT count(*) FROM github_users; + +-- Find all events for a single user. +SELECT created_at, event_type, repo->>'name' AS repo_name +FROM github_events +WHERE user_id = 3861633; + +-- Find the number of commits on the master branch per hour +SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour;`; diff --git a/src/Explorer/Quickstart/QuickstartGuide.tsx b/src/Explorer/Quickstart/QuickstartGuide.tsx index 56465e715..46ed22d8b 100644 --- a/src/Explorer/Quickstart/QuickstartGuide.tsx +++ b/src/Explorer/Quickstart/QuickstartGuide.tsx @@ -11,6 +11,17 @@ import { Text, TextField, } from "@fluentui/react"; +import { + distributeTableCommand, + distributeTableCommandForDisplay, + loadDataCommand, + loadDataCommandForDisplay, + newTableCommand, + newTableCommandForDisplay, + queryCommand, + queryCommandForDisplay, +} from "Explorer/Quickstart/PostgreQuickstartCommands"; +import { useTerminal } from "hooks/useTerminal"; import React, { useState } from "react"; import Youtube from "react-youtube"; import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg"; @@ -35,65 +46,6 @@ enum GuideSteps { export const QuickstartGuide: React.FC = (): JSX.Element => { const [currentStep, setCurrentStep] = useState(0); - const newTableCommand = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE; -CREATE SCHEMA cosmosdb_tutorial; - --- Using schema created for tutorial -SET search_path to cosmosdb_tutorial; - -CREATE TABLE github_users -( - user_id bigint, - url text, - login text, - avatar_url text, - gravatar_id text, - display_login text -); - -CREATE TABLE github_events -( - event_id bigint, - event_type text, - event_public boolean, - repo_id bigint, - payload jsonb, - repo jsonb, - user_id bigint, - org jsonb, - created_at timestamp -); - ---Create indexes on events table -CREATE INDEX event_type_index ON github_events (event_type); -CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops); `; - - const distributeTableCommand = `-- Using schema created for the tutorial -SET search_path to cosmosdb_tutorial; - -SELECT create_distributed_table('github_users', 'user_id'); -SELECT create_distributed_table('github_events', 'user_id'); `; - - const loadDataCommand = `-- Using schema created for the tutorial -SET search_path to cosmosdb_tutorial; - --- download users and store in table -\\COPY github_users FROM PROGRAM 'curl https://examples.citusdata.com/users.csv' WITH (FORMAT CSV) -\\COPY github_events FROM PROGRAM 'curl https://examples.citusdata.com/events.csv' WITH (FORMAT CSV) `; - - const queryCommand = `-- Using schema created for the tutorial -SET search_path to cosmosdb_tutorial; - --- count all rows (across shards) -SELECT count(*) FROM github_users; - --- Find all events for a single user. -SELECT created_at, event_type, repo->>'name' AS repo_name -FROM github_events -WHERE user_id = 3861633; - --- Find the number of commits on the master branch per hour -SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour; `; const onCopyBtnClicked = (selector: string): void => { const textfield: HTMLInputElement = document.querySelector(selector); @@ -143,7 +95,6 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size'):: Quick start guide - Gettings started in Cosmos DB {currentStep < 5 && ( >'distinct_size')::
To begin, please enter the cluster's password in the PostgreSQL terminal. - +
>'distinct_size'):: > Let’s create two tables github_users and github_events in “cosmosdb_tutorial” schema. - Create new table + useTerminal.getState().sendMessage(newTableCommand)} + > + Create new table + >'distinct_size'):: onClick={() => onCopyBtnClicked("#newTableCommand")} /> - + >'distinct_size')::
We are choosing “user_id” as the distribution column for our sample dataset. - Create distributed table + useTerminal.getState().sendMessage(distributeTableCommand)} + > + Create distributed table + >'distinct_size'):: onClick={() => onCopyBtnClicked("#distributeTableCommand")} /> - +
>'distinct_size'):: > Let's load the two tables with a sample dataset generated from the GitHub API. - Load data + useTerminal.getState().sendMessage(loadDataCommand)} + > + Load data + >'distinct_size'):: onClick={() => onCopyBtnClicked("#loadDataCommand")} /> - + >'distinct_size'):: Congratulations on creating and distributing your tables. Now, it's time to run your first query! - Try queries + useTerminal.getState().sendMessage(queryCommand)} + > + Try queries + >'distinct_size'):: onClick={() => onCopyBtnClicked("#queryCommand")} /> - + diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index db034914d..2dfab1d51 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -329,7 +329,7 @@ export class SplashScreen extends React.Component { iconSrc: PowerShellIcon, title: "PostgreSQL Shell", description: "Create table and interact with data using PostgreSQL’s shell interface", - onClick: () => this.container.openNotebookTerminal(TerminalKind.Mongo), + onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres), }; heroes.push(postgreShellBtn); } else { diff --git a/src/Explorer/Tabs/QuickstartTab.tsx b/src/Explorer/Tabs/QuickstartTab.tsx index f07f485ef..12c94b8e3 100644 --- a/src/Explorer/Tabs/QuickstartTab.tsx +++ b/src/Explorer/Tabs/QuickstartTab.tsx @@ -18,7 +18,7 @@ export const QuickstartTab: React.FC = ({ explorer }: Quicks }, []); const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({ authToken: notebookServerInfo.authToken, - notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`, + notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`, forwardingId: notebookServerInfo.forwardingId, }); @@ -32,7 +32,7 @@ export const QuickstartTab: React.FC = ({ explorer }: Quicks )} {!notebookServerInfo?.notebookServerEndpoint && ( diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts index 63ce367d2..6d85c9e7b 100644 --- a/src/Terminal/JupyterLabAppFactory.ts +++ b/src/Terminal/JupyterLabAppFactory.ts @@ -2,7 +2,7 @@ * JupyterLab applications based on jupyterLab components */ import { ServerConnection, TerminalManager } from "@jupyterlab/services"; -import { IMessage } from "@jupyterlab/services/lib/terminal/terminal"; +import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal"; import { Terminal } from "@jupyterlab/terminal"; import { Panel, Widget } from "@phosphor/widgets"; import { userContext } from "UserContext"; @@ -46,7 +46,7 @@ export class JupyterLabAppFactory { } } - public async createTerminalApp(serverSettings: ServerConnection.ISettings) { + public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise { const manager = new TerminalManager({ serverSettings: serverSettings, }); @@ -68,7 +68,7 @@ export class JupyterLabAppFactory { if (!term) { console.error("Failed starting terminal"); - return; + return undefined; } term.title.closable = false; @@ -90,5 +90,7 @@ export class JupyterLabAppFactory { window.addEventListener("unload", () => { panel.dispose(); }); + + return session; } } diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index f71792fab..0fe7b77dd 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -1,4 +1,5 @@ import { ServerConnection } from "@jupyterlab/services"; +import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal"; import "@jupyterlab/terminal/style/index.css"; import { MessageTypes } from "Contracts/ExplorerContracts"; import postRobot from "post-robot"; @@ -41,7 +42,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings return ServerConnection.makeSettings(options); }; -const initTerminal = async (props: TerminalProps) => { +const initTerminal = async (props: TerminalProps): Promise => { // Initialize userContext (only properties which are needed by TelemetryProcessor) updateUserContext({ subscriptionId: props.subscriptionId, @@ -55,10 +56,12 @@ const initTerminal = async (props: TerminalProps) => { const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); try { - await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings); + const session = await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); + return session; } catch (error) { TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); + return undefined; } }; @@ -70,6 +73,7 @@ const closeTab = (tabId: string): void => { }; const main = async (): Promise => { + let session: ITerminalConnection | undefined; postRobot.on( "props", { @@ -80,7 +84,22 @@ const main = async (): Promise => { // Typescript definition for event is wrong. So read props by casting to // eslint-disable-next-line @typescript-eslint/no-explicit-any const props = (event as any).data as TerminalProps; - await initTerminal(props); + session = await initTerminal(props); + } + ); + + postRobot.on( + "sendMessage", + { + window: window.parent, + domain: window.location.origin, + }, + async (event) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = (event as any).data as IMessage; + if (session) { + session.send(message); + } } ); }; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 1a26813d8..186bb7fd9 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -1,4 +1,4 @@ -import { useTabs } from "hooks/useTabs"; +import { ReactTabKind, useTabs } from "hooks/useTabs"; import { useEffect, useState } from "react"; import { applyExplorerBindings } from "../applyExplorerBindings"; import { AuthType } from "../AuthType"; @@ -100,7 +100,11 @@ async function configureHosted(): Promise { } if (event.data?.type === MessageTypes.CloseTab) { - useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); + if (event.data?.data?.tabId === "QuickstartPSQLShell") { + useTabs.getState().closeReactTab(ReactTabKind.Quickstart); + } else { + useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); + } } }, false @@ -290,7 +294,11 @@ async function configurePortal(): Promise { } else if (shouldForwardMessage(message, event.origin)) { sendMessage(message); } else if (event.data?.type === MessageTypes.CloseTab) { - useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); + if (event.data?.data?.tabId === "QuickstartPSQLShell") { + useTabs.getState().closeReactTab(ReactTabKind.Quickstart); + } else { + useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); + } } }, false diff --git a/src/hooks/useTerminal.ts b/src/hooks/useTerminal.ts new file mode 100644 index 000000000..85368531c --- /dev/null +++ b/src/hooks/useTerminal.ts @@ -0,0 +1,26 @@ +import postRobot from "post-robot"; +import create, { UseStore } from "zustand"; + +interface TerminalState { + terminalWindow: Window; + setTerminal: (terminalWindow: Window) => void; + sendMessage: (message: string) => void; +} + +export const useTerminal: UseStore = create((set, get) => ({ + terminalWindow: undefined, + setTerminal: (terminalWindow: Window) => { + set({ terminalWindow }); + }, + sendMessage: (message: string) => { + const terminalWindow = get().terminalWindow; + postRobot.send( + terminalWindow, + "sendMessage", + { type: "stdin", content: [message] }, + { + domain: window.location.origin, + } + ); + }, +}));