Merge branch 'master' of https://github.com/Azure/cosmos-explorer into PSQL_shell_integration

This commit is contained in:
Victor Meng
2022-10-05 17:35:41 -07:00
9 changed files with 263 additions and 44 deletions

View File

@@ -35,6 +35,7 @@ export enum MessageTypes {
RefreshDatabaseAccount, RefreshDatabaseAccount,
CloseTab, CloseTab,
OpenQuickstartBlade, OpenQuickstartBlade,
OpenPostgreSQLPasswordReset,
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@@ -397,6 +397,8 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string; dataExplorerVersion?: string;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
isPostgresAccount?: boolean; isPostgresAccount?: 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[]; flights?: readonly string[];
features?: { features?: {
[key: string]: string; [key: string]: string;

View File

@@ -122,7 +122,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded: userContext.apiType !== "Tables", isSharded: userContext.apiType !== "Tables",
partitionKey: this.getPartitionKey(), partitionKey: this.getPartitionKey(),
enableDedicatedThroughput: false, enableDedicatedThroughput: false,
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"), createMongoWildCardIndex:
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
useHashV2: false, useHashV2: false,
enableAnalyticalStore: false, enableAnalyticalStore: false,
uniqueKeys: [], uniqueKeys: [],
@@ -735,7 +736,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}} }}
> >
<Stack className="panelGroupSpacing" id="collapsibleSectionContent"> <Stack className="panelGroupSpacing" id="collapsibleSectionContent">
{isCapabilityEnabled("EnableMongo") && ( {isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>

View File

@@ -11,6 +11,8 @@ import {
TeachingBubbleContent, TeachingBubbleContent,
Text, Text,
} from "@fluentui/react"; } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { TerminalKind } from "Contracts/ViewModels"; import { TerminalKind } from "Contracts/ViewModels";
import { useCarousel } from "hooks/useCarousel"; import { useCarousel } from "hooks/useCarousel";
import { usePostgres } from "hooks/usePostgres"; import { usePostgres } from "hooks/usePostgres";
@@ -91,6 +93,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
() => this.setState({}), () => this.setState({}),
(state) => state.showPostgreTeachingBubble (state) => state.showPostgreTeachingBubble
), ),
},
{
dispose: usePostgres.subscribe(
() => this.setState({}),
(state) => state.showResetPasswordBubble
),
} }
); );
} }
@@ -118,12 +126,21 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
: "Globally distributed, multi-model database service for any scale"} : "Globally distributed, multi-model database service for any scale"}
</div> </div>
<div className="mainButtonsContainer"> <div className="mainButtonsContainer">
{userContext.apiType === "Postgres" && usePostgres.getState().showPostgreTeachingBubble && ( {userContext.apiType === "Postgres" &&
usePostgres.getState().showPostgreTeachingBubble &&
!usePostgres.getState().showResetPasswordBubble && (
<TeachingBubble <TeachingBubble
headline="New to Cosmos DB PGSQL?" headline="New to Cosmos DB PGSQL?"
target={"#quickstartDescription"} target={"#mainButton-quickstartDescription"}
hasCloseButton hasCloseButton
onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)} onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)}
calloutProps={{
directionalHint: DirectionalHint.rightCenter,
directionalHintFixed: true,
preventDismissOnLostFocus: true,
preventDismissOnResize: true,
preventDismissOnScroll: true,
}}
primaryButtonProps={{ primaryButtonProps={{
text: "Get started", text: "Get started",
onClick: () => { onClick: () => {
@@ -132,12 +149,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}, },
}} }}
> >
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you can Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you
find sample data, query. can find sample data, query.
</TeachingBubble> </TeachingBubble>
)} )}
{mainItems.map((item) => ( {mainItems.map((item) => (
<Stack <Stack
id={`mainButton-${item.id}`}
horizontal horizontal
className="mainButton focusable" className="mainButton focusable"
key={`${item.title}`} key={`${item.title}`}
@@ -161,6 +179,32 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
</div> </div>
</Stack> </Stack>
))} ))}
{userContext.apiType === "Postgres" && usePostgres.getState().showResetPasswordBubble && (
<TeachingBubble
headline="Create your password"
target={"#mainButton-quickstartDescription"}
hasCloseButton
onDismiss={() => usePostgres.getState().setShowResetPasswordBubble(false)}
calloutProps={{
directionalHint: DirectionalHint.bottomRightEdge,
directionalHintFixed: true,
preventDismissOnLostFocus: true,
preventDismissOnResize: true,
preventDismissOnScroll: true,
}}
primaryButtonProps={{
text: "Create",
onClick: () => {
sendMessage({
type: MessageTypes.OpenQuickstartBlade,
});
usePostgres.getState().setShowResetPasswordBubble(false);
},
}}
>
This password will be used to connect to the database.
</TeachingBubble>
)}
</div> </div>
{useCarousel.getState().showCoachMark && ( {useCarousel.getState().showCoachMark && (
<Coachmark <Coachmark

View File

@@ -0,0 +1,133 @@
import {
Checkbox,
Dropdown,
Icon,
IconButton,
IDropdownOption,
ITextFieldStyles,
Label,
Link,
Stack,
Text,
TextField,
TooltipHost,
} from "@fluentui/react";
import React from "react";
import { userContext } from "UserContext";
export const PostgresConnectTab: React.FC = (): JSX.Element => {
const { adminLogin, nodes, enablePublicIpAccess } = userContext.postgresConnectionStrParams;
const [usePgBouncerPort, setUsePgBouncerPort] = React.useState<boolean>(false);
const [selectedNode, setSelectedNode] = React.useState<string>(nodes?.[0]?.value);
const portNumber = usePgBouncerPort ? "6432" : "5432";
const onCopyBtnClicked = (selector: string): void => {
const textfield: HTMLInputElement = document.querySelector(selector);
textfield.select();
document.execCommand("copy");
};
const textfieldStyles: Partial<ITextFieldStyles> = {
root: { width: "100%" },
field: { backgroundColor: "rgb(230, 230, 230)" },
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
subComponentStyles: { label: { fontWeight: 400 } },
description: { fontWeight: 400 },
};
const nodesDropdownOptions: IDropdownOption[] = nodes.map((node) => ({
key: node.value,
text: node.text,
}));
const postgresSQLConnectionURL = `postgres://${adminLogin}:{your_password}@${selectedNode}:${portNumber}/citus?sslmode=require`;
const psql = `psql "host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require"`;
const jdbc = `jdbc:postgresql://${selectedNode}:${portNumber}/citus?user=${adminLogin}&password={your_password}&sslmode=require`;
const libpq = `host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require`;
const adoDotNet = `Server=${selectedNode};Database=citus;Port=${portNumber};User Id=${adminLogin};Password={your_password};Ssl Mode=Require;`;
return (
<div style={{ width: "100%", padding: 16 }}>
<Stack horizontal verticalAlign="center" style={{ marginBottom: 8 }}>
<Label style={{ marginRight: 8 }}>Public IP addresses on worker nodes:</Label>
<TooltipHost
content="
You can enable or disable public IP addresses on the worker nodes on 'Networking' page of your server group."
>
<Icon style={{ margin: "5px 8px 0 0", cursor: "default" }} iconName="Info" />
</TooltipHost>
<TextField value={enablePublicIpAccess ? "On" : "Off"} readOnly disabled />
</Stack>
<Stack horizontal style={{ marginBottom: 8 }}>
<Label style={{ marginRight: 85 }}>Show connection strings for</Label>
<Dropdown
options={nodesDropdownOptions}
selectedKey={selectedNode}
onChange={(_, option) => {
const selectedNode = option.key as string;
setSelectedNode(selectedNode);
if (!selectedNode.startsWith("c.")) {
setUsePgBouncerPort(false);
}
}}
style={{ width: 200 }}
/>
</Stack>
<Stack horizontal style={{ marginBottom: 8 }}>
<Label style={{ marginRight: 44 }}>PgBouncer connection strings</Label>
<Checkbox
boxSide="end"
checked={usePgBouncerPort}
onChange={(_, checked: boolean) => setUsePgBouncerPort(checked)}
disabled={!selectedNode?.startsWith("c.")}
/>
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="PostgreSQL connection URL"
id="postgresSQLConnectionURL"
readOnly
value={postgresSQLConnectionURL}
styles={textfieldStyles}
/>
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#postgresSQLConnectionURL")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField label="psql" id="psql" readOnly value={psql} styles={textfieldStyles} />
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#psql")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField label="JDBC" id="JDBC" readOnly value={jdbc} styles={textfieldStyles} />
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#JDBC")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField
label="Node.js, Python, Ruby, PHP, C++ (libpq)"
id="libpq"
readOnly
value={libpq}
styles={textfieldStyles}
/>
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#libpq")} />
</Stack>
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
<TextField label="ADO.NET" id="adoDotNet" readOnly value={adoDotNet} styles={textfieldStyles} />
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#adoDotNet")} />
</Stack>
<Label>Secure connections</Label>
<Text>
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.{" "}
<Link href="https://go.microsoft.com/fwlink/?linkid=2155061" target="_blank">
Learn more
</Link>
</Text>
</div>
);
};

View File

@@ -2,10 +2,12 @@ import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab"; import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { userContext } from "UserContext";
import loadingIcon from "../../../images/circular_loader_black_16x16.gif"; import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
import errorIcon from "../../../images/close-black.svg"; import errorIcon from "../../../images/close-black.svg";
import { useObservable } from "../../hooks/useObservable"; import { useObservable } from "../../hooks/useObservable";
@@ -189,7 +191,7 @@ const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => {
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => { const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
switch (activeReactTab) { switch (activeReactTab) {
case ReactTabKind.Connect: case ReactTabKind.Connect:
return <ConnectTab />; return userContext.apiType === "Postgres" ? <PostgresConnectTab /> : <ConnectTab />;
case ReactTabKind.Home: case ReactTabKind.Home:
return <SplashScreen explorer={explorer} />; return <SplashScreen explorer={explorer} />;
case ReactTabKind.Quickstart: case ReactTabKind.Quickstart:

View File

@@ -26,6 +26,20 @@ export interface CollectionCreationDefaults {
throughput: ThroughputDefaults; throughput: ThroughputDefaults;
} }
export interface Node {
text: string;
value: string;
ariaLabel: string;
}
export interface PostgresConnectionStrParams {
adminLogin: string;
enablePublicIpAccess: boolean;
nodes: Node[];
isMarlinServerGroup: boolean;
isFreeTier: boolean;
}
interface UserContext { interface UserContext {
readonly authType?: AuthType; readonly authType?: AuthType;
readonly masterKey?: string; readonly masterKey?: string;
@@ -52,6 +66,7 @@ interface UserContext {
collectionId: string; collectionId: string;
partitionKey?: string; partitionKey?: string;
}; };
readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
collectionCreationDefaults: CollectionCreationDefaults; collectionCreationDefaults: CollectionCreationDefaults;
} }
@@ -97,8 +112,10 @@ function updateUserContext(newContext: Partial<UserContext>): void {
if (newContext.apiType === "Postgres") { if (newContext.apiType === "Postgres") {
usePostgres.getState().setShowPostgreTeachingBubble(true); usePostgres.getState().setShowPostgreTeachingBubble(true);
localStorage.setItem(newContext.databaseAccount.id, "true"); localStorage.setItem(newContext.databaseAccount.id, "true");
} else if (userContext.isTryCosmosDBSubscription || isNewAccount) { }
if (userContext.isTryCosmosDBSubscription || isNewAccount) {
useCarousel.getState().setShouldOpen(true); useCarousel.getState().setShouldOpen(true);
usePostgres.getState().setShowResetPasswordBubble(true);
localStorage.setItem(newContext.databaseAccount.id, "true"); localStorage.setItem(newContext.databaseAccount.id, "true");
traceOpen(Action.OpenCarousel); traceOpen(Action.OpenCarousel);
} }
@@ -112,27 +129,28 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
return "SQL"; 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";
}
if (account.kind === "Postgres") {
return "Postgres"; return "Postgres";
}
// const capabilities = account.properties?.capabilities; return "SQL";
// 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 }; export { userContext, updateUserContext };

View File

@@ -28,7 +28,7 @@ import {
} from "../Platform/Hosted/HostedUtils"; } from "../Platform/Hosted/HostedUtils";
import { CollectionCreation } from "../Shared/Constants"; import { CollectionCreation } from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { PortalEnv, updateUserContext, userContext } from "../UserContext"; import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
import { getMsalInstance } from "../Utils/AuthorizationUtils"; import { getMsalInstance } from "../Utils/AuthorizationUtils";
@@ -365,6 +365,20 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.isPostgresAccount) { if (inputs.isPostgresAccount) {
updateUserContext({ apiType: "Postgres" }); updateUserContext({ apiType: "Postgres" });
if (inputs.connectionStringParams) {
// TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension
let flattenedNodesArray: Node[] = [];
inputs.connectionStringParams.nodes?.forEach((node: Node | Node[]) => {
if (Array.isArray(node)) {
flattenedNodesArray = [...flattenedNodesArray, ...node];
} else {
flattenedNodesArray.push(node);
}
});
inputs.connectionStringParams.nodes = flattenedNodesArray;
updateUserContext({ postgresConnectionStrParams: inputs.connectionStringParams });
}
} }
if (inputs.features) { if (inputs.features) {

View File

@@ -2,10 +2,14 @@ import create, { UseStore } from "zustand";
interface TeachingBubbleState { interface TeachingBubbleState {
showPostgreTeachingBubble: boolean; showPostgreTeachingBubble: boolean;
showResetPasswordBubble: boolean;
setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => void; setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => void;
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => void;
} }
export const usePostgres: UseStore<TeachingBubbleState> = create((set) => ({ export const usePostgres: UseStore<TeachingBubbleState> = create((set) => ({
showPostgreTeachingBubble: false, showPostgreTeachingBubble: false,
showResetPasswordBubble: false,
setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => set({ showPostgreTeachingBubble }), setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => set({ showPostgreTeachingBubble }),
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => set({ showResetPasswordBubble }),
})); }));