From ec74c936704d4ca6ef373a0013c6f4ac8be9f9c8 Mon Sep 17 00:00:00 2001 From: Victor Meng Date: Thu, 29 Sep 2022 23:01:19 -0700 Subject: [PATCH] Integrate PSQL shell in quick start guide --- .../Notebook/NotebookTerminalComponent.tsx | 4 +- src/Explorer/Explorer.tsx | 12 +- .../CommandBarComponentButtonFactory.tsx | 16 +-- .../Quickstart/PostgreQuickstartCommands.ts | 105 +++++++++++++++++ src/Explorer/Quickstart/QuickstartGuide.tsx | 106 +++++++----------- src/Explorer/Tabs/QuickstartTab.tsx | 2 +- src/Terminal/JupyterLabAppFactory.ts | 8 +- src/Terminal/index.ts | 25 ++++- src/UserContext.ts | 40 +++---- src/hooks/useTerminal.ts | 26 +++++ 10 files changed, 230 insertions(+), 114 deletions(-) create mode 100644 src/Explorer/Quickstart/PostgreQuickstartCommands.ts create mode 100644 src/hooks/useTerminal.ts diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx index 3fb8c7d2e..e1fe48cbb 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/Quickstart/PostgreQuickstartCommands.ts b/src/Explorer/Quickstart/PostgreQuickstartCommands.ts new file mode 100644 index 000000000..0544d068c --- /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 '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) +`; + +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 '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)`; + +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..e62aa5007 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); @@ -170,14 +122,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'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')::
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'):: > 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'):: Congratulations on creating and distributing your tables. Now, it's time to run your first query! - Try queries + useTerminal.getState().sendMessage(queryCommand)} + > + Try queries + = ({ explorer }: Quicks }, []); const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({ authToken: notebookServerInfo.authToken, - notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`, + notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`, forwardingId: notebookServerInfo.forwardingId, }); diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts index 63ce367d2..9a177a8c9 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..145b43d2e 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; 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/UserContext.ts b/src/UserContext.ts index ee5e7eb41..9fa350116 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -112,25 +112,27 @@ function apiType(account: DatabaseAccount | undefined): ApiType { return "SQL"; } - const capabilities = account.properties?.capabilities; - if (capabilities) { - if (capabilities.find((c) => c.name === "EnableCassandra")) { - return "Cassandra"; - } - if (capabilities.find((c) => c.name === "EnableGremlin")) { - return "Gremlin"; - } - if (capabilities.find((c) => c.name === "EnableMongo")) { - return "Mongo"; - } - if (capabilities.find((c) => c.name === "EnableTable")) { - return "Tables"; - } - } - if (account.kind === "MongoDB" || account.kind === "Parse") { - return "Mongo"; - } - return "SQL"; + return "Postgres"; + + // const capabilities = account.properties?.capabilities; + // if (capabilities) { + // if (capabilities.find((c) => c.name === "EnableCassandra")) { + // return "Cassandra"; + // } + // if (capabilities.find((c) => c.name === "EnableGremlin")) { + // return "Gremlin"; + // } + // if (capabilities.find((c) => c.name === "EnableMongo")) { + // return "Mongo"; + // } + // if (capabilities.find((c) => c.name === "EnableTable")) { + // return "Tables"; + // } + // } + // if (account.kind === "MongoDB" || account.kind === "Parse") { + // return "Mongo"; + // } + // return "SQL"; } export { userContext, updateUserContext }; 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, + } + ); + }, +}));