Integrate PSQL shell in quick start guide (#1333)

This commit is contained in:
victor-meng 2022-10-06 11:32:19 -07:00 committed by GitHub
parent 8433a027ad
commit e909ac43f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 235 additions and 109 deletions

View File

@ -33,6 +33,7 @@ export interface DatabaseAccountExtendedProperties {
privateEndpointConnections?: unknown[]; privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number }; capacity?: { totalThroughputLimit: number };
locations?: DatabaseAccountResponseLocation[]; locations?: DatabaseAccountResponseLocation[];
postgresqlEndpoint?: string;
} }
export interface DatabaseAccountResponseLocation { export interface DatabaseAccountResponseLocation {

View File

@ -2,6 +2,7 @@
* Wrapper around Notebook server terminal * Wrapper around Notebook server terminal
*/ */
import { useTerminal } from "hooks/useTerminal";
import postRobot from "post-robot"; import postRobot from "post-robot";
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
@ -40,6 +41,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void { handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow; this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
useTerminal.getState().setTerminal(this.terminalWindow);
this.sendPropsToTerminalFrame(); this.sendPropsToTerminalFrame();
} }
@ -75,7 +77,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) { } else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint; terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) { } else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
return (this.props.databaseAccount?.properties as any).postgresqlEndpoint; return this.props.databaseAccount?.properties.postgresqlEndpoint;
} }
if (terminalEndpoint) { if (terminalEndpoint) {

View File

@ -186,9 +186,7 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
} }
if (userContext.apiType !== "Postgres") { this.refreshExplorer();
this.refreshExplorer();
}
} }
public async initiateAndRefreshNotebookList(): Promise<void> { public async initiateAndRefreshNotebookList(): Promise<void> {
@ -1249,9 +1247,11 @@ export default class Explorer {
} }
public async refreshExplorer(): Promise<void> { public async refreshExplorer(): Promise<void> {
userContext.authType === AuthType.ResourceToken if (userContext.apiType !== "Postgres") {
? this.refreshDatabaseForResourceToken() userContext.authType === AuthType.ResourceToken
: this.refreshAllDatabases(); ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
}
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount

View File

@ -85,14 +85,11 @@ export function createStaticCommandBarButtons(
(userContext.apiType === "Mongo" && (userContext.apiType === "Mongo" &&
useNotebook.getState().isShellEnabled && useNotebook.getState().isShellEnabled &&
selectedNodeState.isDatabaseNodeOrNoneSelected()) || selectedNodeState.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra" || userContext.apiType === "Cassandra"
userContext.apiType === "Postgres"
) { ) {
notebookButtons.push(createDivider()); notebookButtons.push(createDivider());
if (userContext.apiType === "Cassandra") { if (userContext.apiType === "Cassandra") {
notebookButtons.push(createOpenCassandraTerminalButton(container)); notebookButtons.push(createOpenCassandraTerminalButton(container));
} else if (userContext.apiType === "Postgres") {
notebookButtons.push(createOpenPsqlTerminalButton(container));
} else { } else {
notebookButtons.push(createOpenMongoTerminalButton(container)); notebookButtons.push(createOpenMongoTerminalButton(container));
} }
@ -612,16 +609,7 @@ function createStaticCommandBarButtonsForResourceToken(
} }
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] { export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
const postgreShellLabel = "Open PostgreSQL Shell"; const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
const openPostgreShellBtn = {
iconSrc: HostedTerminalIcon,
iconAlt: postgreShellLabel,
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Mongo),
commandButtonLabel: postgreShellLabel,
hasPopup: false,
disabled: false,
ariaLabel: postgreShellLabel,
};
return [openPostgreShellBtn]; return [openPostgreShellBtn];
} }

View File

@ -124,8 +124,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
} }
const firstWriteLocation = const firstWriteLocation =
databaseAccount?.properties?.writeLocations && userContext.apiType === "Postgres"
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase(); ? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`; const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
try { try {
@ -313,7 +314,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) { if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) { if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true; 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 { } else {
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed; isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
} }

View File

@ -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;`;

View File

@ -11,6 +11,17 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } 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 React, { useState } from "react";
import Youtube from "react-youtube"; import Youtube from "react-youtube";
import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg"; import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg";
@ -35,65 +46,6 @@ enum GuideSteps {
export const QuickstartGuide: React.FC = (): JSX.Element => { export const QuickstartGuide: React.FC = (): JSX.Element => {
const [currentStep, setCurrentStep] = useState<number>(0); const [currentStep, setCurrentStep] = useState<number>(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 onCopyBtnClicked = (selector: string): void => {
const textfield: HTMLInputElement = document.querySelector(selector); const textfield: HTMLInputElement = document.querySelector(selector);
@ -143,7 +95,6 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}> <Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}> <Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
<Text variant="xxLarge">Quick start guide</Text> <Text variant="xxLarge">Quick start guide</Text>
<Text variant="medium">Gettings started in Cosmos DB</Text>
{currentStep < 5 && ( {currentStep < 5 && (
<Pivot style={{ marginTop: 10, width: "100%" }} selectedKey={GuideSteps[currentStep]}> <Pivot style={{ marginTop: 10, width: "100%" }} selectedKey={GuideSteps[currentStep]}>
<PivotItem <PivotItem
@ -159,7 +110,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<br /> <br />
To begin, please enter the cluster&apos;s password in the PostgreSQL terminal. To begin, please enter the cluster&apos;s password in the PostgreSQL terminal.
</Text> </Text>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="UaBDXHMQAUw" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@ -170,14 +121,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
> >
<Stack style={{ marginTop: 20 }}> <Stack style={{ marginTop: 20 }}>
<Text>Lets create two tables github_users and github_events in cosmosdb_tutorial schema.</Text> <Text>Lets create two tables github_users and github_events in cosmosdb_tutorial schema.</Text>
<DefaultButton style={{ marginTop: 16, width: 150 }}>Create new table</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 150 }}
onClick={() => useTerminal.getState().sendMessage(newTableCommand)}
>
Create new table
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="newTableCommand" id="newTableCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={newTableCommand} defaultValue={newTableCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@ -194,7 +150,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#newTableCommand")} onClick={() => onCopyBtnClicked("#newTableCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="VJqupvSQ-mw" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@ -210,14 +166,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<br /> <br />
We are choosing user_id as the distribution column for our sample dataset. We are choosing user_id as the distribution column for our sample dataset.
</Text> </Text>
<DefaultButton style={{ marginTop: 16, width: 200 }}>Create distributed table</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 200 }}
onClick={() => useTerminal.getState().sendMessage(distributeTableCommand)}
>
Create distributed table
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="distributeTableCommand" id="distributeTableCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={distributeTableCommand} defaultValue={distributeTableCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@ -234,7 +195,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#distributeTableCommand")} onClick={() => onCopyBtnClicked("#distributeTableCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="Q-AW7q1GLDY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@ -245,14 +206,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
> >
<Stack style={{ marginTop: 20 }}> <Stack style={{ marginTop: 20 }}>
<Text>Let&apos;s load the two tables with a sample dataset generated from the GitHub API.</Text> <Text>Let&apos;s load the two tables with a sample dataset generated from the GitHub API.</Text>
<DefaultButton style={{ marginTop: 16, width: 110 }}>Load data</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 110 }}
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
>
Load data
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="loadDataCommand" id="loadDataCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={loadDataCommand} defaultValue={loadDataCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@ -269,7 +235,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#loadDataCommand")} onClick={() => onCopyBtnClicked("#loadDataCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="h15fvLKXzRo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem <PivotItem
@ -282,14 +248,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<Text> <Text>
Congratulations on creating and distributing your tables. Now, it&apos;s time to run your first query! Congratulations on creating and distributing your tables. Now, it&apos;s time to run your first query!
</Text> </Text>
<DefaultButton style={{ marginTop: 16, width: 115 }}>Try queries</DefaultButton> <DefaultButton
style={{ marginTop: 16, width: 115 }}
onClick={() => useTerminal.getState().sendMessage(queryCommand)}
>
Try queries
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}> <Stack horizontal style={{ marginTop: 16 }}>
<TextField <TextField
id="queryCommand" id="queryCommand"
multiline multiline
rows={5} rows={5}
readOnly readOnly
defaultValue={queryCommand} defaultValue={queryCommandForDisplay}
styles={{ styles={{
root: { width: "90%" }, root: { width: "90%" },
field: { field: {
@ -306,7 +277,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#queryCommand")} onClick={() => onCopyBtnClicked("#queryCommand")}
/> />
</Stack> </Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} /> <Youtube videoId="p46nRnE4b8Y" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack> </Stack>
</PivotItem> </PivotItem>
</Pivot> </Pivot>

View File

@ -329,7 +329,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: PowerShellIcon, iconSrc: PowerShellIcon,
title: "PostgreSQL Shell", title: "PostgreSQL Shell",
description: "Create table and interact with data using PostgreSQLs shell interface", description: "Create table and interact with data using PostgreSQLs shell interface",
onClick: () => this.container.openNotebookTerminal(TerminalKind.Mongo), onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
}; };
heroes.push(postgreShellBtn); heroes.push(postgreShellBtn);
} else { } else {

View File

@ -18,7 +18,7 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
}, []); }, []);
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({ const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken, authToken: notebookServerInfo.authToken,
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`, notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
forwardingId: notebookServerInfo.forwardingId, forwardingId: notebookServerInfo.forwardingId,
}); });
@ -32,7 +32,7 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
<NotebookTerminalComponent <NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()} notebookServerInfo={getNotebookServerInfo()}
databaseAccount={userContext.databaseAccount} databaseAccount={userContext.databaseAccount}
tabId="EmbbedTerminal" tabId="QuickstartPSQLShell"
/> />
)} )}
{!notebookServerInfo?.notebookServerEndpoint && ( {!notebookServerInfo?.notebookServerEndpoint && (

View File

@ -2,7 +2,7 @@
* JupyterLab applications based on jupyterLab components * JupyterLab applications based on jupyterLab components
*/ */
import { ServerConnection, TerminalManager } from "@jupyterlab/services"; 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 { Terminal } from "@jupyterlab/terminal";
import { Panel, Widget } from "@phosphor/widgets"; import { Panel, Widget } from "@phosphor/widgets";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
@ -46,7 +46,7 @@ export class JupyterLabAppFactory {
} }
} }
public async createTerminalApp(serverSettings: ServerConnection.ISettings) { public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise<ITerminalConnection | undefined> {
const manager = new TerminalManager({ const manager = new TerminalManager({
serverSettings: serverSettings, serverSettings: serverSettings,
}); });
@ -68,7 +68,7 @@ export class JupyterLabAppFactory {
if (!term) { if (!term) {
console.error("Failed starting terminal"); console.error("Failed starting terminal");
return; return undefined;
} }
term.title.closable = false; term.title.closable = false;
@ -90,5 +90,7 @@ export class JupyterLabAppFactory {
window.addEventListener("unload", () => { window.addEventListener("unload", () => {
panel.dispose(); panel.dispose();
}); });
return session;
} }
} }

View File

@ -1,4 +1,5 @@
import { ServerConnection } from "@jupyterlab/services"; import { ServerConnection } from "@jupyterlab/services";
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
import "@jupyterlab/terminal/style/index.css"; import "@jupyterlab/terminal/style/index.css";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import postRobot from "post-robot"; import postRobot from "post-robot";
@ -41,7 +42,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings
return ServerConnection.makeSettings(options); return ServerConnection.makeSettings(options);
}; };
const initTerminal = async (props: TerminalProps) => { const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection | undefined> => {
// Initialize userContext (only properties which are needed by TelemetryProcessor) // Initialize userContext (only properties which are needed by TelemetryProcessor)
updateUserContext({ updateUserContext({
subscriptionId: props.subscriptionId, subscriptionId: props.subscriptionId,
@ -55,10 +56,12 @@ const initTerminal = async (props: TerminalProps) => {
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
try { 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); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
return session;
} catch (error) { } catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
return undefined;
} }
}; };
@ -70,6 +73,7 @@ const closeTab = (tabId: string): void => {
}; };
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
let session: ITerminalConnection | undefined;
postRobot.on( postRobot.on(
"props", "props",
{ {
@ -80,7 +84,22 @@ const main = async (): Promise<void> => {
// Typescript definition for event is wrong. So read props by casting to <any> // Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = (event as any).data as TerminalProps; 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);
}
} }
); );
}; };

View File

@ -1,4 +1,4 @@
import { useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@ -100,7 +100,11 @@ async function configureHosted(): Promise<Explorer> {
} }
if (event.data?.type === MessageTypes.CloseTab) { 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 false
@ -290,7 +294,11 @@ async function configurePortal(): Promise<Explorer> {
} else if (shouldForwardMessage(message, event.origin)) { } else if (shouldForwardMessage(message, event.origin)) {
sendMessage(message); sendMessage(message);
} else if (event.data?.type === MessageTypes.CloseTab) { } 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 false

26
src/hooks/useTerminal.ts Normal file
View File

@ -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<TerminalState> = 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,
}
);
},
}));