Compare commits

...

12 Commits

Author SHA1 Message Date
Victor Meng
acd5974024 Hard code to always open old mongo shell 2022-12-07 10:21:08 -08:00
Victor Meng
22f5fc13b2 Empty commit to trigger new build 2022-11-14 12:42:07 -08:00
victor-meng
5dde66b032 Add check and guidance for networking settings (#1348) 2022-11-10 10:46:55 -08:00
jawelton74
5b1db2778c Fix typo in 'tutorial' in Quick start. (#1346) 2022-10-27 09:59:14 -07:00
Armando Trejo Oliver
1213788f9c Remove share-link feature (#1345)
* Remove share-link feature

Cosmos DB supports sharing access to an account data using AAD/RBAC now so this feature is unnecessary.

* Fix format issues

* Fix unit tests

* undo package changes
2022-10-20 10:58:37 -07:00
victor-meng
1ce3adff0f Hide launch quick start button and postgres teaching bubbles if account is a replica (#1344) 2022-10-19 17:12:41 -07:00
victor-meng
00eb07da11 Add retries when checking if account is allowed to access phoenix (#1342) 2022-10-19 17:12:24 -07:00
victor-meng
afe59c1589 Postgres fixes (#1341) 2022-10-11 16:03:58 -07:00
victor-meng
53b5ebd39c Add firewall notification in quickstart tab (#1337) 2022-10-10 19:30:52 -07:00
sunghyunkang1111
5b365e642f show introductory video and password reset for first time try postgresql (#1338) (#1340) 2022-10-10 18:53:54 -05:00
victor-meng
333b3de587 Add new message type for opening postgres networking blade (#1336) 2022-10-06 14:25:10 -07:00
victor-meng
e909ac43f4 Integrate PSQL shell in quick start guide (#1333) 2022-10-06 11:32:19 -07:00
36 changed files with 534 additions and 297 deletions

BIN
images/firewallRule.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -33,6 +33,8 @@ export interface DatabaseAccountExtendedProperties {
privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number };
locations?: DatabaseAccountResponseLocation[];
postgresqlEndpoint?: string;
publicNetworkAccess?: string;
}
export interface DatabaseAccountResponseLocation {
@@ -566,6 +568,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",

View File

@@ -36,6 +36,8 @@ export enum MessageTypes {
CloseTab,
OpenQuickstartBlade,
OpenPostgreSQLPasswordReset,
OpenPostgresNetworkingBlade,
OpenCosmosDBNetworkingBlade,
}
export { Versions, ActionContracts, Diagnostics };

View File

@@ -186,7 +186,6 @@ export interface Collection extends CollectionBase {
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
}
@@ -397,6 +396,7 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string;
defaultCollectionThroughput?: CollectionCreationDefaults;
isPostgresAccount?: boolean;
isReplica?: boolean;
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
connectionStringParams?: any;
flights?: readonly string[];

View File

@@ -85,11 +85,7 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
selectedCollection && selectedCollection.onNewMongoShellClick();
},
label: useNotebook.getState().isShellEnabled ? "Open Mongo Shell" : "New Shell",
});

View File

@@ -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<NotebookTerminalC
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): 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<NotebookTerminalC
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
return (this.props.databaseAccount?.properties as any).postgresqlEndpoint;
return this.props.databaseAccount?.properties.postgresqlEndpoint;
}
if (terminalEndpoint) {

View File

@@ -186,9 +186,7 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
}
if (userContext.apiType !== "Postgres") {
this.refreshExplorer();
}
this.refreshExplorer();
}
public async initiateAndRefreshNotebookList(): Promise<void> {
@@ -1249,9 +1247,11 @@ export default class Explorer {
}
public async refreshExplorer(): Promise<void> {
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

View File

@@ -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));
}
@@ -169,11 +166,7 @@ export function createContextCommandBarButtons(
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
selectedCollection && selectedCollection.onNewMongoShellClick();
},
commandButtonLabel: label,
ariaLabel: label,
@@ -612,16 +605,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];
}

View File

@@ -124,8 +124,9 @@ export const useNotebook: UseStore<NotebookState> = 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<NotebookState> = 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;
}

View File

@@ -1,17 +1,9 @@
jest.mock("../hooks/useFullScreenURLs");
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
import { OpenFullScreen } from "./OpenFullScreen";
it("renders the correct URLs", () => {
(useFullScreenURLs as jest.Mock).mockReturnValue({
readWrite: "read and write url",
read: "read only url",
});
render(<OpenFullScreen />);
expect(screen.getByLabelText("Read and Write")).toHaveValue("https://cosmos.azure.com/?key=read and write url");
expect(screen.getByLabelText("Read Only")).toHaveValue("https://cosmos.azure.com/?key=read only url");
expect(screen.getByText("Open")).toBeDefined();
});

View File

@@ -1,66 +1,26 @@
import { DefaultButton, PrimaryButton, Spinner, Stack, Text, TextField } from "@fluentui/react";
import copyToClipboard from "clipboard-copy";
import { PrimaryButton, Stack, Text } from "@fluentui/react";
import * as React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
export const OpenFullScreen: React.FunctionComponent = () => {
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
const result = useFullScreenURLs();
if (!result) {
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
}
const readWriteUrl = `https://cosmos.azure.com/?key=${result.readWrite}`;
const readUrl = `https://cosmos.azure.com/?key=${result.read}`;
return (
<>
<Stack tokens={{ childrenGap: 10 }}>
<Text>
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read only
access urls below to share with others. For security purposes, the URLs grant time-bound access to the
account. When access expires, you can reconnect, using a valid connection string for the account.
</Text>
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
onClick={() => {
copyToClipboard(readWriteUrl);
setIsReadWriteUrlCopy(true);
}}
text={isReadWriteUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readWriteUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
<div style={{ padding: "34px" }}>
<Stack tokens={{ childrenGap: 10 }}>
<Text>
Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your
Microsoft account or a connection string.
</Text>
<Stack horizontal tokens={{ childrenGap: 10 }}>
<PrimaryButton
onClick={() => {
window.open("https://cosmos.azure.com/", "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack>
</Stack>
<TextField label="Read Only" readOnly defaultValue={readUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
onClick={() => {
setIsReadUrlCopy(true);
copyToClipboard(readUrl);
}}
text={isReadUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
onClick={() => {
window.open(readUrl, "_blank");
}}
text="Open"
iconProps={{ iconName: "OpenInNewWindow" }}
/>
</Stack>
</Stack>
</div>
</>
);
};

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

@@ -94,7 +94,7 @@ const getDescriptionText = (page: number): string => {
case 1:
return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. ";
case 2:
return "Launch the quickstart for a tutotrial to learn how to create a database, add sample data, connect to a sample app and more.";
return "Launch the quickstart for a tutorial to learn how to create a database, add sample data, connect to a sample app and more.";
case 3:
return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer.";
default:

View File

@@ -0,0 +1,22 @@
import { Image, PrimaryButton, Stack, Text } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import React from "react";
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
export const QuickstartFirewallNotification: React.FC = (): JSX.Element => (
<Stack style={{ padding: "16px 20px" }}>
<Text block>
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).
</Text>
<Text block>We strongly recommend removing this rule once you finish using the PostgreSQL shell.</Text>
<Image style={{ margin: "20px 0" }} src={FirewallRuleScreenshot} />
<PrimaryButton
style={{ width: 150 }}
onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}
>
Add firewall rule
</PrimaryButton>
</Stack>
);

View File

@@ -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<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 textfield: HTMLInputElement = document.querySelector(selector);
@@ -143,14 +95,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
<Text variant="xxLarge">Quick start guide</Text>
<Text variant="medium">Gettings started in Cosmos DB</Text>
{currentStep < 5 && (
<Pivot style={{ marginTop: 10, width: "100%" }} selectedKey={GuideSteps[currentStep]}>
<Pivot
style={{ marginTop: 10, width: "100%" }}
selectedKey={GuideSteps[currentStep]}
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
>
<PivotItem
headerText="Login"
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
itemKey={GuideSteps[0]}
onClick={() => setCurrentStep(0)}
onClick={() => {
setCurrentStep(0);
}}
>
<Stack style={{ marginTop: 20 }}>
<Text>
@@ -158,8 +115,12 @@ 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.
<br />
<br />
Note: If you navigate out of the Quick Start tab (PostgreSQL Shell), the session will be closed and
all ongoing commands might be interrupted.
</Text>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
<Youtube videoId="nT64dFSfiUo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack>
</PivotItem>
<PivotItem
@@ -169,15 +130,20 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => setCurrentStep(1)}
>
<Stack style={{ marginTop: 20 }}>
<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>
<Text>Let&apos;s create two tables github_users and github_events in cosmosdb_tutorial schema.</Text>
<DefaultButton
style={{ marginTop: 16, width: 150 }}
onClick={() => useTerminal.getState().sendMessage(newTableCommand)}
>
Create new table
</DefaultButton>
<Stack horizontal style={{ marginTop: 16 }}>
<TextField
id="newTableCommand"
multiline
rows={5}
readOnly
defaultValue={newTableCommand}
defaultValue={newTableCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
@@ -194,7 +160,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#newTableCommand")}
/>
</Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
<Youtube videoId="il_sA6U1WcY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack>
</PivotItem>
<PivotItem
@@ -210,14 +176,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<br />
We are choosing user_id as the distribution column for our sample dataset.
</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 }}>
<TextField
id="distributeTableCommand"
multiline
rows={5}
readOnly
defaultValue={distributeTableCommand}
defaultValue={distributeTableCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
@@ -234,7 +205,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#distributeTableCommand")}
/>
</Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
<Youtube videoId="kCCDRRrN1r0" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack>
</PivotItem>
<PivotItem
@@ -245,14 +216,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
>
<Stack style={{ marginTop: 20 }}>
<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 }}>
<TextField
id="loadDataCommand"
multiline
rows={5}
readOnly
defaultValue={loadDataCommand}
defaultValue={loadDataCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
@@ -269,7 +245,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#loadDataCommand")}
/>
</Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
<Youtube videoId="XSMEE2tujEk" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack>
</PivotItem>
<PivotItem
@@ -282,14 +258,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
<Text>
Congratulations on creating and distributing your tables. Now, it&apos;s time to run your first query!
</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 }}>
<TextField
id="queryCommand"
multiline
rows={5}
readOnly
defaultValue={queryCommand}
defaultValue={queryCommandForDisplay}
styles={{
root: { width: "90%" },
field: {
@@ -306,7 +287,7 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
onClick={() => onCopyBtnClicked("#queryCommand")}
/>
</Stack>
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
<Youtube videoId="k_EanjMtaPg" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
</Stack>
</PivotItem>
</Pivot>

View File

@@ -14,10 +14,6 @@
padding-right: 16px;
max-width: 1168px;
> * {
justify-content: space-between;
}
> .title {
position: relative; // To attach FeaturePanelLauncher as absolute
color: @BaseHigh;

View File

@@ -186,7 +186,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
headline="Create your password"
target={"#mainButton-quickstartDescription"}
hasCloseButton
onDismiss={() => usePostgres.getState().setShowResetPasswordBubble(false)}
onDismiss={() => {
localStorage.setItem(userContext.databaseAccount.id, "true");
usePostgres.getState().setShowResetPasswordBubble(false);
}}
calloutProps={{
directionalHint: DirectionalHint.bottomRightEdge,
directionalHintFixed: true,
@@ -197,14 +200,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
primaryButtonProps={{
text: "Create",
onClick: () => {
localStorage.setItem(userContext.databaseAccount.id, "true");
sendMessage({
type: MessageTypes.OpenQuickstartBlade,
type: MessageTypes.OpenPostgreSQLPasswordReset,
});
usePostgres.getState().setShowResetPasswordBubble(false);
},
}}
>
This password will be used to connect to the database.
If you haven&apos;t changed your password yet, change it now.
</TeachingBubble>
)}
</div>
@@ -300,7 +304,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
public createMainItems(): SplashScreenItem[] {
const heroes: SplashScreenItem[] = [];
if (userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Postgres") {
if (
userContext.apiType === "SQL" ||
userContext.apiType === "Mongo" ||
(userContext.apiType === "Postgres" && !userContext.isReplica)
) {
const launchQuickstartBtn = {
id: "quickstartDescription",
iconSrc: QuickStartIcon,
@@ -329,7 +337,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: PowerShellIcon,
title: "PostgreSQL Shell",
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);
} else {
@@ -347,10 +355,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
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),
};

View File

@@ -120,7 +120,7 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking
</Stack>
<Label>Secure connections</Label>
<Text>
<Text style={{ marginBottom: 8 }}>
Only secure connections are supported. For production use cases, we recommend using the &apos;verify-full&apos;
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
</Link>
</Text>
<Label>Connect with pgAdmin</Label>
<Text>
Refer to our{" "}
<Link
href="https://learn.microsoft.com/en-us/azure/postgresql/hyperscale/howto-connect?tabs=pgadmin"
target="_blank"
>
guide
</Link>{" "}
to help you connect via pgAdmin.
</Text>
</div>
);
};

View File

@@ -1,11 +1,15 @@
import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
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,14 +17,42 @@ interface QuickstartTabProps {
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken,
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
forwardingId: notebookServerInfo.forwardingId,
});
const checkFirewallRules = async (): Promise<void> => {
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();
}, []);
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken,
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`,
forwardingId: notebookServerInfo.forwardingId,
});
return (
<Stack style={{ width: "100%" }} horizontal>
@@ -28,15 +60,24 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
<QuickstartGuide />
</Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{notebookServerInfo?.notebookServerEndpoint && (
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}
databaseAccount={userContext.databaseAccount}
tabId="EmbbedTerminal"
tabId="QuickstartPSQLShell"
/>
)}
{!notebookServerInfo?.notebookServerEndpoint && (
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
<Stack style={{ margin: "auto 0" }}>
<Text block style={{ margin: "auto" }}>
Connecting to the PostgreSQL shell.
</Text>
<Text block style={{ margin: "auto" }}>
If the cluster was just created, this could take up to a minute.
</Text>
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
</Stack>
)}
</Stack>
</Stack>

View File

@@ -1,3 +1,6 @@
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
@@ -21,10 +24,24 @@ interface TabsProps {
}
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
const { openedTabs, openedReactTabs, activeTab, activeReactTab, showNetworkSettingsWarning } = useTabs();
return (
<div className="tabsManagerContainer">
{showNetworkSettingsWarning && (
<MessageBar
messageBarType={MessageBarType.warning}
actions={
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenCosmosDBNetworkingBlade })}>
Change network settings
</MessageBarButton>
}
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
>
The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure
Portal to proceed.
</MessageBar>
)}
<div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">

View File

@@ -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<boolean>
) {}
public renderComponent(): JSX.Element {
if (!this.isAllPublicIPAddressesEnabled()) {
return <QuickstartFirewallNotification />;
}
return this.parameters() ? (
<NotebookTerminalComponent
notebookServerInfo={this.getNotebookServerInfo()}
@@ -46,25 +54,33 @@ export default class TerminalTab extends TabsBase {
public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> ';
private container: Explorer;
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
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<boolean>(() => {
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<void> {
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);
}
}
}

View File

@@ -1160,23 +1160,6 @@ export default class Collection implements ViewModels.Collection {
this.onDocumentDBDocumentsClick();
}
/**
* Get correct collection label depending on account API
*/
public getLabel(): string {
if (userContext.apiType === "Tables") {
return "Entities";
} else if (userContext.apiType === "Cassandra") {
return "Rows";
} else if (userContext.apiType === "Gremlin") {
return "Graph";
} else if (userContext.apiType === "Mongo") {
return "Documents";
}
return "Items";
}
public getDatabase(): ViewModels.Database {
return useDatabases.getState().findDatabaseWithId(this.databaseId);
}

View File

@@ -1,5 +1,6 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as React from "react";
import { getItemName } from "Utils/APITypeUtils";
import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg";
@@ -497,7 +498,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
const children: TreeNode[] = [];
children.push({
label: collection.getLabel(),
label: getItemName(),
id: collection.isSampleCollection ? "sampleItems" : "",
onClick: () => {
collection.openTab();

View File

@@ -1,6 +1,7 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as ko from "knockout";
import * as React from "react";
import { getItemName } from "Utils/APITypeUtils";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg";
@@ -254,7 +255,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
const children: TreeNode[] = [];
children.push({
label: collection.getLabel(),
label: getItemName(),
onClick: () => {
collection.openTab();
// push to most recent

View File

@@ -45,7 +45,11 @@ export class PhoenixClient {
}
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
return this.executeContainerAssignmentOperation(provisionData, "allocate");
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
retries: 4,
maxTimeout: 20000,
minTimeout: 20000,
});
}
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
@@ -80,9 +84,12 @@ export class PhoenixClient {
}
const phoenixError = responseJson as IPhoenixError;
if (response.status === HttpStatusCodes.Forbidden) {
throw new Error(this.ConvertToForbiddenErrorString(phoenixError));
if (phoenixError.message === "Sequence contains no elements") {
throw Error("Phoenix container allocation failed, please try again later.");
}
throw new AbortError(this.ConvertToForbiddenErrorString(phoenixError));
}
throw new Error(phoenixError.message);
throw new AbortError(phoenixError.message);
} catch (error) {
error.status = response?.status;
throw error;

View File

@@ -1,31 +0,0 @@
import * as Constants from "../../Common/Constants";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { userContext } from "../../UserContext";
export default class AuthHeadersUtil {
public static async generateEncryptedToken(readOnly: boolean = false): Promise<DataModels.GenerateTokenResponse> {
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
const headers: any = { authorization: userContext.authorizationToken };
headers[Constants.HttpHeaders.getReadOnlyKey] = readOnly;
const response = await fetch(url, { method: "POST", headers });
const result = await response.json();
// This API has a quirk where the response must be parsed to JSON twice
return JSON.parse(result);
}
private static _generateResourceUrl(): string {
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType);
const accountEndpoint = databaseAccount?.properties?.documentEndpoint || "";
const sid = subscriptionId || "";
const rg = resourceGroup || "";
const dba = databaseAccount?.name || "";
const resourceUrl = encodeURIComponent(accountEndpoint);
const rid = "";
const rtype = "";
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
}
}

View File

@@ -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<ITerminalConnection | undefined> {
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;
@@ -81,6 +81,9 @@ export class JupyterLabAppFactory {
// Attach the widget to the dom.
Widget.attach(panel, document.body);
// Switch focus to the terminal
term.activate();
// Handle resize events.
window.addEventListener("resize", () => {
panel.update();
@@ -90,5 +93,7 @@ export class JupyterLabAppFactory {
window.addEventListener("unload", () => {
panel.dispose();
});
return session;
}
}

View File

@@ -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<ITerminalConnection | undefined> => {
// 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<void> => {
let session: ITerminalConnection | undefined;
postRobot.on(
"props",
{
@@ -80,7 +84,22 @@ const main = async (): Promise<void> => {
// Typescript definition for event is wrong. So read props by casting to <any>
// 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);
}
}
);
};

View File

@@ -67,6 +67,7 @@ interface UserContext {
partitionKey?: string;
};
readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
readonly isReplica?: boolean;
collectionCreationDefaults: CollectionCreationDefaults;
}
@@ -109,16 +110,19 @@ function updateUserContext(newContext: Partial<UserContext>): void {
);
if (!localStorage.getItem(newContext.databaseAccount.id)) {
if (newContext.apiType === "Postgres") {
if (newContext.isTryCosmosDBSubscription || isNewAccount) {
if (newContext.apiType === "Postgres" && !newContext.isReplica) {
usePostgres.getState().setShowResetPasswordBubble(true);
usePostgres.getState().setShowPostgreTeachingBubble(true);
} else {
useCarousel.getState().setShouldOpen(true);
localStorage.setItem(newContext.databaseAccount.id, "true");
traceOpen(Action.OpenCarousel);
}
} else if (newContext.apiType === "Postgres") {
usePostgres.getState().setShowPostgreTeachingBubble(true);
localStorage.setItem(newContext.databaseAccount.id, "true");
}
if (userContext.isTryCosmosDBSubscription || isNewAccount) {
useCarousel.getState().setShouldOpen(true);
usePostgres.getState().setShowResetPasswordBubble(true);
localStorage.setItem(newContext.databaseAccount.id, "true");
traceOpen(Action.OpenCarousel);
}
}
}
Object.assign(userContext, newContext);

View File

@@ -74,3 +74,18 @@ export const getApiShortDisplayName = (): string => {
return "Table API";
}
};
export const getItemName = (): string => {
switch (userContext.apiType) {
case "Tables":
return "Entities";
case "Cassandra":
return "Rows";
case "Gremlin":
return "Graph";
case "Mongo":
return "Documents";
default:
return "Items";
}
};

View File

@@ -0,0 +1,40 @@
import { userContext } from "UserContext";
const PortalIPs: { [key: string]: string[] } = {
prod1: ["104.42.195.92", "40.76.54.131"],
prod2: ["104.42.196.69"],
mooncake: ["139.217.8.252"],
blackforest: ["51.4.229.218"],
fairfax: ["52.244.48.71"],
ussec: ["29.26.26.67", "29.26.26.66"],
usnat: ["7.28.202.68"],
};
export const doNetworkSettingsAllowDataExplorerAccess = (): boolean => {
const accountProperties = userContext.databaseAccount?.properties;
if (!accountProperties) {
return false;
}
// public network access is disabled
if (accountProperties.publicNetworkAccess !== "Enabled") {
return false;
}
const ipRules = accountProperties.ipRules;
// public network access is set to "All networks"
if (ipRules.length === 0) {
return true;
}
const portalIPs = PortalIPs[userContext.portalEnv];
let numberOfMatches = 0;
ipRules.forEach((ipRule) => {
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
numberOfMatches++;
}
});
return numberOfMatches === portalIPs.length;
};

View File

@@ -1,18 +0,0 @@
import { useEffect, useState } from "react";
import { GenerateTokenResponse } from "../Contracts/DataModels";
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
export function useFullScreenURLs(): GenerateTokenResponse | undefined {
const [state, setState] = useState<GenerateTokenResponse>();
useEffect(() => {
Promise.all([AuthHeadersUtil.generateEncryptedToken(), AuthHeadersUtil.generateEncryptedToken(true)]).then(
([readWriteResponse, readOnlyResponse]) =>
setState({
readWrite: readWriteResponse.readWrite,
read: readOnlyResponse.read,
})
);
}, []);
return state;
}

View File

@@ -1,5 +1,6 @@
import { useTabs } from "hooks/useTabs";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react";
import { doNetworkSettingsAllowDataExplorerAccess } from "Utils/NetworkUtility";
import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType";
import { AccountKind, Flights } from "../Common/Constants";
@@ -100,7 +101,11 @@ async function configureHosted(): Promise<Explorer> {
}
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 +295,11 @@ async function configurePortal(): Promise<Explorer> {
} 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
@@ -356,7 +365,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
});
if (inputs.isPostgresAccount) {
updateUserContext({ apiType: "Postgres" });
updateUserContext({ apiType: "Postgres", isReplica: !!inputs.isReplica });
if (inputs.connectionStringParams) {
// TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension
@@ -373,6 +382,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
}
}
const isDataExplorerAccessAllowed = doNetworkSettingsAllowDataExplorerAccess();
useTabs.getState().setShowNetworkSettingsWarning(!isDataExplorerAccessAllowed);
if (inputs.features) {
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
}

View File

@@ -9,6 +9,7 @@ interface TabsState {
openedReactTabs: ReactTabKind[];
activeTab: TabsBase | undefined;
activeReactTab: ReactTabKind | undefined;
showNetworkSettingsWarning: boolean;
activateTab: (tab: TabsBase) => void;
activateNewTab: (tab: TabsBase) => void;
activateReactTab: (tabkind: ReactTabKind) => void;
@@ -20,6 +21,7 @@ interface TabsState {
closeAllNotebookTabs: (hardClose: boolean) => void;
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
closeReactTab: (tabKind: ReactTabKind) => void;
setShowNetworkSettingsWarning: (showWarning: boolean) => void;
}
export enum ReactTabKind {
@@ -33,6 +35,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedReactTabs: [ReactTabKind.Home],
activeTab: undefined,
activeReactTab: ReactTabKind.Home,
showNetworkSettingsWarning: false,
activateTab: (tab: TabsBase): void => {
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
set({ activeTab: tab, activeReactTab: undefined });
@@ -142,4 +145,5 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
set({ openedReactTabs: updatedOpenedReactTabs });
},
setShowNetworkSettingsWarning: (showWarning: boolean) => set({ showNetworkSettingsWarning: showWarning }),
}));

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

View File

@@ -82,7 +82,6 @@
"./src/Explorer/Tree/AccessibleVerticalList.ts",
"./src/GitHub/GitHubConnector.ts",
"./src/HostedExplorerChildFrame.ts",
"./src/Platform/Hosted/Authorization.ts",
"./src/Platform/Hosted/Components/MeControl.test.tsx",
"./src/Platform/Hosted/Components/MeControl.tsx",
"./src/Platform/Hosted/Components/SignInButton.tsx",
@@ -126,7 +125,6 @@
"./src/Utils/WindowUtils.ts",
"./src/hooks/useConfig.ts",
"./src/hooks/useDirectories.tsx",
"./src/hooks/useFullScreenURLs.tsx",
"./src/hooks/useGraphPhoto.tsx",
"./src/hooks/useNotebookSnapshotStore.ts",
"./src/hooks/usePortalAccessToken.tsx",
@@ -165,4 +163,4 @@
"src/Terminal/**/*",
"src/Utils/arm/**/*"
]
}
}