mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
25 Commits
PSQL_Shell
...
accessibil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a660551c6 | ||
|
|
b2d59f3b3f | ||
|
|
850f1dfb97 | ||
|
|
a827e79317 | ||
|
|
7dbccff41d | ||
|
|
9184684e75 | ||
|
|
701f486d8f | ||
|
|
5059917edf | ||
|
|
ab1409efb1 | ||
|
|
5de9e682ba | ||
|
|
b2ab979360 | ||
|
|
2f32a676d0 | ||
|
|
950c8ee470 | ||
|
|
b0eaac5b84 | ||
|
|
952491a3ad | ||
|
|
5dde66b032 | ||
|
|
5b1db2778c | ||
|
|
1213788f9c | ||
|
|
1ce3adff0f | ||
|
|
00eb07da11 | ||
|
|
afe59c1589 | ||
|
|
53b5ebd39c | ||
|
|
5b365e642f | ||
|
|
333b3de587 | ||
|
|
e909ac43f4 |
BIN
images/firewallRule.png
Normal file
BIN
images/firewallRule.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -6,7 +6,7 @@
|
|||||||
display: table;
|
display: table;
|
||||||
display: none;
|
display: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px solid #DDDDDD;
|
border-top: 1px solid #dddddd;
|
||||||
/*[{environment-commandbar-toolbar-separator}]*/
|
/*[{environment-commandbar-toolbar-separator}]*/
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
/*[{plugin-background-color}]*/
|
/*[{plugin-background-color}]*/
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
/*[{plugin-background-color}]*/
|
/*[{plugin-background-color}]*/
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-bottom: 1px solid #DDDDDD;
|
border-bottom: 1px solid #dddddd;
|
||||||
/*[1px solid {environment-commandbar-toolbar-separator}]*/
|
/*[1px solid {environment-commandbar-toolbar-separator}]*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,14 +70,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
|
||||||
background-color: #CCCEDB;
|
background-color: #cccedb;
|
||||||
/*[{common-controls-button-hover-background}]*/
|
/*[{common-controls-button-hover-background}]*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
|
||||||
background-color: #E6E7ED;
|
background-color: #e6e7ed;
|
||||||
/*[{common-controls-inner-tab-active-background}]*/
|
/*[{common-controls-inner-tab-active-background}]*/
|
||||||
outline: none
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
|
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
.query-editor-text {
|
.query-editor-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
border: solid 1px #A9ACB3;
|
border: solid 1px #a9acb3;
|
||||||
/*[{plugin-textbox-disabled-color}]*/
|
/*[{plugin-textbox-disabled-color}]*/
|
||||||
resize: none;
|
resize: none;
|
||||||
margin-top: -39px;
|
margin-top: -39px;
|
||||||
@@ -201,9 +201,9 @@ input::-webkit-inner-spin-button {
|
|||||||
.advanced-options-panel .advanced-options .top .top-input {
|
.advanced-options-panel .advanced-options .top .top-input {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
word-spacing: normal;
|
word-spacing: normal;
|
||||||
color: #1E1E1E;
|
color: #1e1e1e;
|
||||||
/*[{common-controls-button-foreground}]*/
|
/*[{common-controls-button-foreground}]*/
|
||||||
border: 1px solid #CCCEDB;
|
border: 1px solid #cccedb;
|
||||||
/*[1px solid {plugin-textbox-border-color}]*/
|
/*[1px solid {plugin-textbox-border-color}]*/
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -299,9 +299,9 @@ input::-webkit-inner-spin-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scroll-box {
|
.scroll-box {
|
||||||
border-bottom: 1px transparent #DDD;
|
border-bottom: 1px transparent #ddd;
|
||||||
/*[1px solid {plugin-table-border-color}]*/
|
/*[1px solid {plugin-table-border-color}]*/
|
||||||
border-top: 1px transparent #DDD;
|
border-top: 1px transparent #ddd;
|
||||||
/*[1px solid {plugin-table-border-color}]*/
|
/*[1px solid {plugin-table-border-color}]*/
|
||||||
max-height: 20vh;
|
max-height: 20vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -366,7 +366,7 @@ input::-webkit-inner-spin-button {
|
|||||||
|
|
||||||
.group-indicator-table {
|
.group-indicator-table {
|
||||||
border-spacing: 0px;
|
border-spacing: 0px;
|
||||||
min-height: 24px
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-indicator-column {
|
.group-indicator-column {
|
||||||
@@ -396,7 +396,6 @@ input::-webkit-inner-spin-button {
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*.type-header {
|
/*.type-header {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
@@ -410,9 +409,9 @@ input::-webkit-inner-spin-button {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
.clause-table-field[readonly] {
|
.clause-table-field[readonly] {
|
||||||
background-color: #EEEEF2;
|
background-color: #eeeef2;
|
||||||
/*[{plugin-table-header-background-color}]*/
|
/*[{plugin-table-header-background-color}]*/
|
||||||
border: 1px solid #CCCEDB;
|
border: 1px solid #cccedb;
|
||||||
/*[{plugin-table-border-color}]*/
|
/*[{plugin-table-border-color}]*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,11 +461,11 @@ input::-webkit-inner-spin-button {
|
|||||||
|
|
||||||
.query-panel .divider.horizontal {
|
.query-panel .divider.horizontal {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 100%
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-div {
|
.inline-div {
|
||||||
display: inline
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.querybuilder-addpropertyImg,
|
.querybuilder-addpropertyImg,
|
||||||
@@ -485,7 +484,7 @@ input::-webkit-inner-spin-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.entity-Add-Cancel {
|
.entity-Add-Cancel {
|
||||||
padding: @DefaultSpace @SmallSpace @SmallSpace;
|
// padding: @DefaultSpace @SmallSpace @SmallSpace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +497,7 @@ input::-webkit-inner-spin-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.query-builder-isDisabled {
|
.query-builder-isDisabled {
|
||||||
border: 1px solid #CCCEDB;
|
border: 1px solid #cccedb;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,7 +514,6 @@ input::-webkit-inner-spin-button {
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1200px) {
|
||||||
.clause-table {
|
.clause-table {
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export class ArmResourceTypes {
|
|||||||
|
|
||||||
export class BackendDefaults {
|
export class BackendDefaults {
|
||||||
public static partitionKeyKind = "Hash";
|
public static partitionKeyKind = "Hash";
|
||||||
|
public static partitionKeyMultiHash = "MultiHash";
|
||||||
|
public static maxNumMultiHashPartition = 2;
|
||||||
public static singlePartitionStorageInGb: string = "10";
|
public static singlePartitionStorageInGb: string = "10";
|
||||||
public static multiPartitionStorageInGb: string = "100";
|
public static multiPartitionStorageInGb: string = "100";
|
||||||
public static maxChangeFeedRetentionDuration: number = 10;
|
public static maxChangeFeedRetentionDuration: number = 10;
|
||||||
|
|||||||
@@ -73,6 +73,17 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
|
|
||||||
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
|
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
|
||||||
|
|
||||||
|
const handleKeyPress = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (event.key === "Enter" || event.key === "Space") {
|
||||||
|
onEditEntity();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleKeyPressdelete = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (event.key === "Enter" || event.key === "Space") {
|
||||||
|
onDeleteEntity();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getEntityValueType = (): string => {
|
const getEntityValueType = (): string => {
|
||||||
const { Int, Smallint, Tinyint } = CassandraType;
|
const { Int, Smallint, Tinyint } = CassandraType;
|
||||||
const { Double, Int32, Int64 } = TableType;
|
const { Double, Int32, Int64 } = TableType;
|
||||||
@@ -126,12 +137,28 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
|||||||
/>
|
/>
|
||||||
{!isEntityValueDisable && (
|
{!isEntityValueDisable && (
|
||||||
<TooltipHost content="Edit property" id="editTooltip">
|
<TooltipHost content="Edit property" id="editTooltip">
|
||||||
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
|
<Image
|
||||||
|
{...imageProps}
|
||||||
|
src={EditIcon}
|
||||||
|
alt="editEntity"
|
||||||
|
id="editEntity"
|
||||||
|
onClick={onEditEntity}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
)}
|
)}
|
||||||
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
|
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
|
||||||
<TooltipHost content="Delete property" id="deleteTooltip">
|
<TooltipHost content="Delete property" id="deleteTooltip">
|
||||||
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} />
|
<Image
|
||||||
|
{...imageProps}
|
||||||
|
src={DeleteIcon}
|
||||||
|
alt="delete entity"
|
||||||
|
id="deleteEntity"
|
||||||
|
onClick={onDeleteEntity}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyPress={handleKeyPressdelete}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
|||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<TooltipHost content={children}>
|
<TooltipHost content={children}>
|
||||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
|
|||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { getEntityName } from "../DocumentUtility";
|
import { getEntityName } from "../DocumentUtility";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||||
|
|
||||||
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
|
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
|
||||||
const entityName: string = getEntityName();
|
const entityName: string = getEntityName();
|
||||||
@@ -13,7 +14,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
|||||||
await client()
|
await client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||||
.delete();
|
.delete();
|
||||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
12
src/Common/dataAccess/getPartitionKeyValue.ts
Normal file
12
src/Common/dataAccess/getPartitionKeyValue.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { userContext } from "UserContext";
|
||||||
|
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||||
|
|
||||||
|
export const getPartitionKeyValue = (documentId: DocumentId) => {
|
||||||
|
if (userContext.apiType === "Tables" && documentId.partitionKeyValue?.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (documentId.partitionKeyValue?.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return documentId.partitionKeyValue;
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { HttpHeaders } from "../Constants";
|
|||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { getEntityName } from "../DocumentUtility";
|
import { getEntityName } from "../DocumentUtility";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||||
|
|
||||||
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
|
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
|
||||||
const entityName = getEntityName();
|
const entityName = getEntityName();
|
||||||
@@ -21,8 +22,7 @@ export const readDocument = async (collection: CollectionBase, documentId: Docum
|
|||||||
const response = await client()
|
const response = await client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
// use undefined if the partitionKeyValue is empty
|
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
|
||||||
.read(options);
|
.read(options);
|
||||||
|
|
||||||
return response?.resource;
|
return response?.resource;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
|
|||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { getEntityName } from "../DocumentUtility";
|
import { getEntityName } from "../DocumentUtility";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||||
|
|
||||||
export const updateDocument = async (
|
export const updateDocument = async (
|
||||||
collection: CollectionBase,
|
collection: CollectionBase,
|
||||||
@@ -25,7 +26,7 @@ export const updateDocument = async (
|
|||||||
const response = await client()
|
const response = await client()
|
||||||
.database(collection.databaseId)
|
.database(collection.databaseId)
|
||||||
.container(collection.id())
|
.container(collection.id())
|
||||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||||
.replace(newDocument, options);
|
.replace(newDocument, options);
|
||||||
|
|
||||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export interface DatabaseAccountExtendedProperties {
|
|||||||
privateEndpointConnections?: unknown[];
|
privateEndpointConnections?: unknown[];
|
||||||
capacity?: { totalThroughputLimit: number };
|
capacity?: { totalThroughputLimit: number };
|
||||||
locations?: DatabaseAccountResponseLocation[];
|
locations?: DatabaseAccountResponseLocation[];
|
||||||
|
postgresqlEndpoint?: string;
|
||||||
|
publicNetworkAccess?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccountResponseLocation {
|
export interface DatabaseAccountResponseLocation {
|
||||||
@@ -566,6 +568,16 @@ export interface ContainerConnectionInfo {
|
|||||||
//need to add ram and rom info
|
//need to add ram and rom info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PostgresFirewallRule {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
startIpAddress: string;
|
||||||
|
endIpAddress: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export enum PhoenixErrorType {
|
export enum PhoenixErrorType {
|
||||||
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
|
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
|
||||||
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
|
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export enum MessageTypes {
|
|||||||
CloseTab,
|
CloseTab,
|
||||||
OpenQuickstartBlade,
|
OpenQuickstartBlade,
|
||||||
OpenPostgreSQLPasswordReset,
|
OpenPostgreSQLPasswordReset,
|
||||||
|
OpenPostgresNetworkingBlade,
|
||||||
|
OpenCosmosDBNetworkingBlade,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
@@ -186,7 +186,6 @@ export interface Collection extends CollectionBase {
|
|||||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||||
|
|
||||||
getLabel(): string;
|
|
||||||
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +396,8 @@ export interface DataExplorerInputsFrame {
|
|||||||
dataExplorerVersion?: string;
|
dataExplorerVersion?: string;
|
||||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||||
isPostgresAccount?: boolean;
|
isPostgresAccount?: boolean;
|
||||||
|
isReplica?: boolean;
|
||||||
|
clientIpAddress?: string;
|
||||||
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
|
// 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;
|
connectionStringParams?: any;
|
||||||
flights?: readonly string[];
|
flights?: readonly string[];
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { HoverCard, HoverCardType, Icon, Label, Link, Stack } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "@fluentui/react";
|
|
||||||
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
||||||
import "./InfoComponent.less";
|
import "./InfoComponent.less";
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export class InfoComponent extends React.Component<InfoComponentProps> {
|
|||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
|
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
|
||||||
<div className="infoPanelMain">
|
<div className="infoPanelMain" tabIndex={0}>
|
||||||
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
|
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
|
||||||
<Label className="infoLabelMain">Help</Label>
|
<Label className="infoLabelMain">Help</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ exports[`InfoComponent renders 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="infoPanelMain"
|
className="infoPanelMain"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className="infoIconMain"
|
className="infoIconMain"
|
||||||
|
|||||||
@@ -310,7 +310,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.isLargePartitionKeyEnabled() && <Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>}
|
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
|
||||||
|
<Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ interface ThroughputInputAutoPilotV3State {
|
|||||||
spendAckChecked: boolean;
|
spendAckChecked: boolean;
|
||||||
exceedFreeTierThroughput: boolean;
|
exceedFreeTierThroughput: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||||
ThroughputInputAutoPilotV3Props,
|
ThroughputInputAutoPilotV3Props,
|
||||||
ThroughputInputAutoPilotV3State
|
ThroughputInputAutoPilotV3State
|
||||||
@@ -624,7 +623,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{warningMessage && (
|
{warningMessage && (
|
||||||
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
|
<MessageBar
|
||||||
|
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
{warningMessage}
|
{warningMessage}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
"iconName": "WarningSolid",
|
"iconName": "WarningSolid",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
role="alert"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
styles={
|
styles={
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ describe("ThroughputInput Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should switch mode properly", () => {
|
it("should switch mode properly", () => {
|
||||||
wrapper.find('[aria-label="Manual mode"]').simulate("change");
|
wrapper.find('[aria-label="Manual database throughput"]').simulate("change");
|
||||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
|
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
|
||||||
"Container throughput (400 - unlimited RU/s)"
|
"Container throughput (400 - unlimited RU/s)"
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.find('[aria-label="Autoscale mode"]').simulate("change");
|
wrapper.find('[aria-label="Autoscale database throughput"]').simulate("change");
|
||||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,8 +186,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<input
|
<input
|
||||||
|
id="Autoscale-input"
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
aria-label="Autoscale mode"
|
aria-label="Autoscale database throughput"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
checked={isAutoscaleSelected}
|
checked={isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -195,11 +196,14 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
|
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
|
||||||
/>
|
/>
|
||||||
<span className="throughputInputRadioBtnLabel">Autoscale</span>
|
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
|
||||||
|
Autoscale
|
||||||
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
id="Manual-input"
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
aria-label="Manual mode"
|
aria-label="Manual database throughput"
|
||||||
checked={!isAutoscaleSelected}
|
checked={!isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
@@ -207,14 +211,20 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||||
/>
|
/>
|
||||||
<span className="throughputInputRadioBtnLabel">Manual</span>
|
<label className="throughputInputRadioBtnLabel" htmlFor="Manual-input">
|
||||||
|
Manual
|
||||||
|
</label>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{isAutoscaleSelected && (
|
{isAutoscaleSelected && (
|
||||||
<Stack className="throughputInputSpacing">
|
<Stack className="throughputInputSpacing">
|
||||||
<Text variant="small" aria-label="ruDescription">
|
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
|
||||||
Estimate your required RU/s with{" "}
|
Estimate your required RU/s with{" "}
|
||||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="ruDescription">
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
|
>
|
||||||
capacity calculator
|
capacity calculator
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -344,13 +344,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
>
|
>
|
||||||
<StyledIconBase
|
<StyledIconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<IconBase
|
<IconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
@@ -630,7 +630,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Info"
|
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||||
className="panelInfoIcon root-57"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
@@ -655,39 +655,43 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
className="ms-Stack css-58"
|
className="ms-Stack css-58"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-label="Autoscale mode"
|
aria-label="Autoscale database throughput"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
checked={true}
|
checked={true}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
|
id="Autoscale-input"
|
||||||
key=".0:$.0"
|
key=".0:$.0"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="radio"
|
type="radio"
|
||||||
/>
|
/>
|
||||||
<span
|
<label
|
||||||
className="throughputInputRadioBtnLabel"
|
className="throughputInputRadioBtnLabel"
|
||||||
|
htmlFor="Autoscale-input"
|
||||||
key=".0:$.1"
|
key=".0:$.1"
|
||||||
>
|
>
|
||||||
Autoscale
|
Autoscale
|
||||||
</span>
|
</label>
|
||||||
<input
|
<input
|
||||||
aria-label="Manual mode"
|
aria-label="Manual database throughput"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
checked={false}
|
checked={false}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
|
id="Manual-input"
|
||||||
key=".0:$.2"
|
key=".0:$.2"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="radio"
|
type="radio"
|
||||||
/>
|
/>
|
||||||
<span
|
<label
|
||||||
className="throughputInputRadioBtnLabel"
|
className="throughputInputRadioBtnLabel"
|
||||||
|
htmlFor="Manual-input"
|
||||||
key=".0:$.3"
|
key=".0:$.3"
|
||||||
>
|
>
|
||||||
Manual
|
Manual
|
||||||
</span>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
@@ -697,23 +701,23 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
className="ms-Stack throughputInputSpacing css-59"
|
className="ms-Stack throughputInputSpacing css-59"
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
aria-label="ruDescription"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
key=".0:$.0"
|
key=".0:$.0"
|
||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-label="ruDescription"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="css-54"
|
className="css-54"
|
||||||
>
|
>
|
||||||
Estimate your required RU/s with
|
Estimate your required RU/s with
|
||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
aria-label="ruDescription"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<LinkBase
|
<LinkBase
|
||||||
aria-label="ruDescription"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -992,7 +996,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="ruDescription"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="ms-Link root-60"
|
className="ms-Link root-60"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
@@ -1331,13 +1335,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
>
|
>
|
||||||
<StyledIconBase
|
<StyledIconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<IconBase
|
<IconBase
|
||||||
ariaLabel="Info"
|
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
@@ -1617,7 +1621,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-label="Info"
|
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||||
className="panelInfoIcon root-57"
|
className="panelInfoIcon root-57"
|
||||||
data-icon-name="Info"
|
data-icon-name="Info"
|
||||||
role="img"
|
role="img"
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ describe("ContainerSampleGenerator", () => {
|
|||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rejects with error that contains experience
|
|
||||||
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -186,10 +186,8 @@ 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> {
|
||||||
if (!this.notebookManager) {
|
if (!this.notebookManager) {
|
||||||
@@ -1249,9 +1247,11 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async refreshExplorer(): Promise<void> {
|
public async refreshExplorer(): Promise<void> {
|
||||||
|
if (userContext.apiType !== "Postgres") {
|
||||||
userContext.authType === AuthType.ResourceToken
|
userContext.authType === AuthType.ResourceToken
|
||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: this.refreshAllDatabases();
|
: this.refreshAllDatabases();
|
||||||
|
}
|
||||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||||
|
|
||||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
jest.mock("../hooks/useFullScreenURLs");
|
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
|
||||||
import { OpenFullScreen } from "./OpenFullScreen";
|
import { OpenFullScreen } from "./OpenFullScreen";
|
||||||
|
|
||||||
it("renders the correct URLs", () => {
|
it("renders the correct URLs", () => {
|
||||||
(useFullScreenURLs as jest.Mock).mockReturnValue({
|
|
||||||
readWrite: "read and write url",
|
|
||||||
read: "read only url",
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<OpenFullScreen />);
|
render(<OpenFullScreen />);
|
||||||
expect(screen.getByLabelText("Read and Write")).toHaveValue("https://cosmos.azure.com/?key=read and write url");
|
expect(screen.getByText("Open")).toBeDefined();
|
||||||
expect(screen.getByLabelText("Read Only")).toHaveValue("https://cosmos.azure.com/?key=read only url");
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,66 +1,26 @@
|
|||||||
import { DefaultButton, PrimaryButton, Spinner, Stack, Text, TextField } from "@fluentui/react";
|
import { PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||||
import copyToClipboard from "clipboard-copy";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
|
||||||
|
|
||||||
export const OpenFullScreen: React.FunctionComponent = () => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div style={{ padding: "34px" }}>
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
<Text>
|
<Text>
|
||||||
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read only
|
Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your
|
||||||
access urls below to share with others. For security purposes, the URLs grant time-bound access to the
|
Microsoft account or a connection string.
|
||||||
account. When access expires, you can reconnect, using a valid connection string for the account.
|
|
||||||
</Text>
|
</Text>
|
||||||
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
|
|
||||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||||
<DefaultButton
|
|
||||||
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
|
|
||||||
onClick={() => {
|
|
||||||
copyToClipboard(readWriteUrl);
|
|
||||||
setIsReadWriteUrlCopy(true);
|
|
||||||
}}
|
|
||||||
text={isReadWriteUrlCopy ? "Copied" : "Copy"}
|
|
||||||
iconProps={{ iconName: "Copy" }}
|
|
||||||
/>
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(readWriteUrl, "_blank");
|
window.open("https://cosmos.azure.com/", "_blank");
|
||||||
}}
|
|
||||||
text="Open"
|
|
||||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
|
||||||
/>
|
|
||||||
</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"
|
text="Open"
|
||||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,9 +89,10 @@ export interface AddCollectionPanelState {
|
|||||||
enableIndexing: boolean;
|
enableIndexing: boolean;
|
||||||
isSharded: boolean;
|
isSharded: boolean;
|
||||||
partitionKey: string;
|
partitionKey: string;
|
||||||
|
subPartitionKeys: string[];
|
||||||
enableDedicatedThroughput: boolean;
|
enableDedicatedThroughput: boolean;
|
||||||
createMongoWildCardIndex: boolean;
|
createMongoWildCardIndex: boolean;
|
||||||
useHashV2: boolean;
|
useHashV1: boolean;
|
||||||
enableAnalyticalStore: boolean;
|
enableAnalyticalStore: boolean;
|
||||||
uniqueKeys: string[];
|
uniqueKeys: string[];
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
@@ -121,10 +122,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
enableIndexing: true,
|
enableIndexing: true,
|
||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: this.getPartitionKey(),
|
partitionKey: this.getPartitionKey(),
|
||||||
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
||||||
useHashV2: false,
|
useHashV1: false,
|
||||||
enableAnalyticalStore: false,
|
enableAnalyticalStore: false,
|
||||||
uniqueKeys: [],
|
uniqueKeys: [],
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
@@ -260,7 +262,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
true
|
true
|
||||||
).toLocaleLowerCase()}.`}
|
).toLocaleLowerCase()}.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
|
true
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -336,7 +345,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
true
|
true
|
||||||
).toLocaleLowerCase()} within the database.`}
|
).toLocaleLowerCase()} within the database.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||||
|
true
|
||||||
|
).toLocaleLowerCase()} within the database.`}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -384,7 +400,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -467,7 +488,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={
|
||||||
|
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||||
|
}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -514,7 +542,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={this.getPartitionKeyTooltipText()}
|
content={this.getPartitionKeyTooltipText()}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={this.getPartitionKeyTooltipText()}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -546,6 +579,77 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{userContext.apiType === "SQL" &&
|
||||||
|
this.state.subPartitionKeys.map((subPartitionKey: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "20px",
|
||||||
|
border: "solid",
|
||||||
|
borderWidth: "0px 0px 1px 1px",
|
||||||
|
marginRight: "5px",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="addCollection-partitionKeyValue"
|
||||||
|
key={`addCollection-partitionKeyValue_${index}`}
|
||||||
|
aria-required
|
||||||
|
required
|
||||||
|
size={40}
|
||||||
|
tabIndex={index > 0 ? 1 : 0}
|
||||||
|
className="panelTextField"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
||||||
|
aria-label={this.getPartitionKeyName()}
|
||||||
|
pattern={".*"}
|
||||||
|
title={""}
|
||||||
|
value={subPartitionKey}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const subPartitionKeys = [...this.state.subPartitionKeys];
|
||||||
|
if (!this.state.subPartitionKeys[index] && !event.target.value.startsWith("/")) {
|
||||||
|
subPartitionKeys[index] = "/" + event.target.value.trim();
|
||||||
|
this.setState({ subPartitionKeys });
|
||||||
|
} else {
|
||||||
|
subPartitionKeys[index] = event.target.value.trim();
|
||||||
|
this.setState({ subPartitionKeys });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27 }}
|
||||||
|
onClick={() => {
|
||||||
|
const subPartitionKeys = this.state.subPartitionKeys.filter((uniqueKey, j) => index !== j);
|
||||||
|
this.setState({ subPartitionKeys });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{userContext.apiType === "SQL" && (
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<DefaultButton
|
||||||
|
styles={{ root: { padding: 0, width: 250, height: 30 }, label: { fontSize: 12 } }}
|
||||||
|
hidden={this.state.useHashV1}
|
||||||
|
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||||
|
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
|
||||||
|
>
|
||||||
|
Add hierarchical partition key (preview)
|
||||||
|
</DefaultButton>
|
||||||
|
{this.state.subPartitionKeys.length > 0 && (
|
||||||
|
<Text variant="small">
|
||||||
|
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
||||||
|
partition your data with up to three levels of keys for better data distribution. Requires preview
|
||||||
|
version of .NET V3 or Java V4 SDK.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -572,7 +676,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||||
|
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||||
|
true
|
||||||
|
).toLocaleLowerCase()} in the database and
|
||||||
|
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||||
|
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -603,11 +717,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="Unique keys provide developers with the ability to add a layer of data integrity to their database. By
|
content={
|
||||||
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||||
per partition key."
|
}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={
|
||||||
|
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||||
|
}
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -670,7 +791,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={this.getAnalyticalStorageTooltipContent()}
|
content={this.getAnalyticalStorageTooltipContent()}
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||||
|
impacting the performance of transactional workloads."
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -747,7 +874,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||||
>
|
>
|
||||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||||
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -767,18 +899,29 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="My partition key is larger than 101 bytes"
|
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||||
checked={this.state.useHashV2}
|
checked={this.state.useHashV1}
|
||||||
styles={{
|
styles={{
|
||||||
text: { fontSize: 12 },
|
text: { fontSize: 12 },
|
||||||
checkbox: { width: 12, height: 12 },
|
checkbox: { width: 12, height: 12 },
|
||||||
label: { padding: 0, alignItems: "center" },
|
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
||||||
}}
|
}}
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||||
this.setState({ useHashV2: isChecked })
|
this.setState({ useHashV1: isChecked, subPartitionKeys: [] })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Text variant="small">
|
||||||
|
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> To ensure compatibility with
|
||||||
|
older SDKs, the created container will use a legacy partitioning scheme that supports partition
|
||||||
|
key values of size only up to 101 bytes. If this is enabled, you will not be able to use
|
||||||
|
hierarchical partition keys.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
@@ -833,12 +976,20 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPartitionKeyPlaceHolder(): string {
|
private getPartitionKeyPlaceHolder(index?: number): string {
|
||||||
switch (userContext.apiType) {
|
switch (userContext.apiType) {
|
||||||
case "Mongo":
|
case "Mongo":
|
||||||
return "e.g., address.zipCode";
|
return "e.g., address.zipCode";
|
||||||
case "Gremlin":
|
case "Gremlin":
|
||||||
return "e.g., /address";
|
return "e.g., /address";
|
||||||
|
case "SQL":
|
||||||
|
return `${
|
||||||
|
index === undefined
|
||||||
|
? "Required - first partition key e.g., /TenantId"
|
||||||
|
: index === 0
|
||||||
|
? "second partition key e.g., /UserId"
|
||||||
|
: "third partition key e.g., /SessionId"
|
||||||
|
}`;
|
||||||
default:
|
default:
|
||||||
return "e.g., /address/zipCode";
|
return "e.g., /address/zipCode";
|
||||||
}
|
}
|
||||||
@@ -1164,11 +1315,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||||
const partitionKeyVersion = this.state.useHashV2 ? 2 : undefined;
|
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
paths: [partitionKeyString],
|
paths: [
|
||||||
kind: "Hash",
|
partitionKeyString,
|
||||||
|
...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0
|
||||||
|
? this.state.subPartitionKeys
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||||||
version: partitionKeyVersion,
|
version: partitionKeyVersion,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setLoadingFalse();
|
setLoadingFalse();
|
||||||
setFormError(error);
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
setFormError(errorMessage);
|
||||||
TelemetryProcessor.traceFailure(
|
TelemetryProcessor.traceFailure(
|
||||||
Action.DeleteDatabase,
|
Action.DeleteDatabase,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
|||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
{showErrorDetails && (
|
{showErrorDetails && (
|
||||||
<a className="paneErrorLink" role="link" onClick={expandConsole}>
|
<a className="paneErrorLink" role="link" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
|
||||||
More details
|
More details
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -242,6 +242,11 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
submitButtonText: getButtonLabel(userContext.apiType),
|
submitButtonText: getButtonLabel(userContext.apiType),
|
||||||
onSubmit,
|
onSubmit,
|
||||||
};
|
};
|
||||||
|
const handlekeypressaddentity = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (event.key === "Enter" || event.key === "Space") {
|
||||||
|
addNewEntity();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
@@ -284,7 +289,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{userContext.apiType !== "Cassandra" && (
|
{userContext.apiType !== "Cassandra" && (
|
||||||
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
|
<Stack
|
||||||
|
horizontal
|
||||||
|
onClick={addNewEntity}
|
||||||
|
className="addButtonEntiy"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyPress={handlekeypressaddentity}
|
||||||
|
>
|
||||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
||||||
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
|||||||
className="addButtonEntiy"
|
className="addButtonEntiy"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-Stack addButtonEntiy css-53"
|
className="ms-Stack addButtonEntiy css-53"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
onKeyPress={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<StyledImageBase
|
<StyledImageBase
|
||||||
alt="Add Entity"
|
alt="Add Entity"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
directionalHint={4}
|
directionalHint={4}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaLabel="A database is analogous to a namespace. It is the unit of management for a set of containers."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -124,6 +125,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
directionalHint={4}
|
directionalHint={4}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaLabel="Throughput configured at the database level will be shared across all containers within the database."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -163,6 +165,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
directionalHint={4}
|
directionalHint={4}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaLabel="Unique identifier for the container and used for id-based routing through REST and all SDKs."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -206,6 +209,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
directionalHint={4}
|
directionalHint={4}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaLabel="The partition key is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume. For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -223,13 +227,36 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
id="addCollection-partitionKeyValue"
|
id="addCollection-partitionKeyValue"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
pattern=".*"
|
pattern=".*"
|
||||||
placeholder="e.g., /address/zipCode"
|
placeholder="Required - first partition key e.g., /TenantId"
|
||||||
required={true}
|
required={true}
|
||||||
size={40}
|
size={40}
|
||||||
title=""
|
title=""
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
|
<Stack
|
||||||
|
className="panelGroupSpacing"
|
||||||
|
>
|
||||||
|
<CustomizedDefaultButton
|
||||||
|
disabled={false}
|
||||||
|
hidden={false}
|
||||||
|
onClick={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"height": 30,
|
||||||
|
"padding": 0,
|
||||||
|
"width": 250,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add hierarchical partition key (preview)
|
||||||
|
</CustomizedDefaultButton>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
@@ -246,6 +273,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
directionalHint={4}
|
directionalHint={4}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaLabel="Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -303,6 +331,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
directionalHint={4}
|
directionalHint={4}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -395,10 +424,13 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
<Stack
|
<Stack
|
||||||
className="panelGroupSpacing"
|
className="panelGroupSpacing"
|
||||||
id="collapsibleSectionContent"
|
id="collapsibleSectionContent"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="panelGroupSpacing"
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
label="My partition key is larger than 101 bytes"
|
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
@@ -409,6 +441,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
"label": Object {
|
"label": Object {
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"padding": 0,
|
"padding": 0,
|
||||||
|
"whiteSpace": "break-spaces",
|
||||||
|
"wordWrap": "break-word",
|
||||||
},
|
},
|
||||||
"text": Object {
|
"text": Object {
|
||||||
"fontSize": 12,
|
"fontSize": 12,
|
||||||
@@ -416,6 +450,24 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Text
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="removeIcon"
|
||||||
|
iconName="InfoSolid"
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/cosmos-large-pk"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
105
src/Explorer/Quickstart/PostgreQuickstartCommands.ts
Normal file
105
src/Explorer/Quickstart/PostgreQuickstartCommands.ts
Normal 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;`;
|
||||||
@@ -94,7 +94,7 @@ const getDescriptionText = (page: number): string => {
|
|||||||
case 1:
|
case 1:
|
||||||
return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. ";
|
return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. ";
|
||||||
case 2:
|
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:
|
case 3:
|
||||||
return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer.";
|
return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer.";
|
||||||
default:
|
default:
|
||||||
|
|||||||
22
src/Explorer/Quickstart/QuickstartFirewallNotification.tsx
Normal file
22
src/Explorer/Quickstart/QuickstartFirewallNotification.tsx
Normal 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>
|
||||||
|
);
|
||||||
@@ -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,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={{ 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]}
|
||||||
|
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
|
||||||
|
>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
headerText="Login"
|
headerText="Login"
|
||||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
|
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
|
||||||
itemKey={GuideSteps[0]}
|
itemKey={GuideSteps[0]}
|
||||||
onClick={() => setCurrentStep(0)}
|
onClick={() => {
|
||||||
|
setCurrentStep(0);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Stack style={{ marginTop: 20 }}>
|
<Stack style={{ marginTop: 20 }}>
|
||||||
<Text>
|
<Text>
|
||||||
@@ -158,8 +115,12 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
To begin, please enter the cluster's password in the PostgreSQL terminal.
|
To begin, please enter the cluster'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>
|
</Text>
|
||||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
<Youtube videoId="nT64dFSfiUo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
@@ -169,15 +130,20 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
|||||||
onClick={() => setCurrentStep(1)}
|
onClick={() => setCurrentStep(1)}
|
||||||
>
|
>
|
||||||
<Stack style={{ marginTop: 20 }}>
|
<Stack style={{ marginTop: 20 }}>
|
||||||
<Text>Let’s create two tables github_users and github_events in “cosmosdb_tutorial” schema.</Text>
|
<Text>Let's 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 +160,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="il_sA6U1WcY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
@@ -210,14 +176,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 +205,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="kCCDRRrN1r0" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
@@ -245,14 +216,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
|||||||
>
|
>
|
||||||
<Stack style={{ marginTop: 20 }}>
|
<Stack style={{ marginTop: 20 }}>
|
||||||
<Text>Let's load the two tables with a sample dataset generated from the GitHub API.</Text>
|
<Text>Let'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 +245,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="XSMEE2tujEk" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
<PivotItem
|
||||||
@@ -282,14 +258,19 @@ SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::
|
|||||||
<Text>
|
<Text>
|
||||||
Congratulations on creating and distributing your tables. Now, it's time to run your first query!
|
Congratulations on creating and distributing your tables. Now, it'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 +287,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="k_EanjMtaPg" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
</Pivot>
|
</Pivot>
|
||||||
|
|||||||
@@ -14,10 +14,6 @@
|
|||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
max-width: 1168px;
|
max-width: 1168px;
|
||||||
|
|
||||||
> * {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title {
|
> .title {
|
||||||
position: relative; // To attach FeaturePanelLauncher as absolute
|
position: relative; // To attach FeaturePanelLauncher as absolute
|
||||||
color: @BaseHigh;
|
color: @BaseHigh;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
TeachingBubble,
|
TeachingBubble,
|
||||||
TeachingBubbleContent,
|
TeachingBubbleContent,
|
||||||
Text,
|
Text
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
@@ -186,7 +186,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
headline="Create your password"
|
headline="Create your password"
|
||||||
target={"#mainButton-quickstartDescription"}
|
target={"#mainButton-quickstartDescription"}
|
||||||
hasCloseButton
|
hasCloseButton
|
||||||
onDismiss={() => usePostgres.getState().setShowResetPasswordBubble(false)}
|
onDismiss={() => {
|
||||||
|
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||||
|
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||||
|
}}
|
||||||
calloutProps={{
|
calloutProps={{
|
||||||
directionalHint: DirectionalHint.bottomRightEdge,
|
directionalHint: DirectionalHint.bottomRightEdge,
|
||||||
directionalHintFixed: true,
|
directionalHintFixed: true,
|
||||||
@@ -197,14 +200,15 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
primaryButtonProps={{
|
primaryButtonProps={{
|
||||||
text: "Create",
|
text: "Create",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||||
sendMessage({
|
sendMessage({
|
||||||
type: MessageTypes.OpenQuickstartBlade,
|
type: MessageTypes.OpenPostgreSQLPasswordReset,
|
||||||
});
|
});
|
||||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
This password will be used to connect to the database.
|
If you haven't changed your password yet, change it now.
|
||||||
</TeachingBubble>
|
</TeachingBubble>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -300,7 +304,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
public createMainItems(): SplashScreenItem[] {
|
public createMainItems(): SplashScreenItem[] {
|
||||||
const heroes: 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 = {
|
const launchQuickstartBtn = {
|
||||||
id: "quickstartDescription",
|
id: "quickstartDescription",
|
||||||
iconSrc: QuickStartIcon,
|
iconSrc: QuickStartIcon,
|
||||||
@@ -329,7 +337,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 PostgreSQL’s shell interface",
|
description: "Create table and interact with data using PostgreSQL’s shell interface",
|
||||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.Mongo),
|
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
|
||||||
};
|
};
|
||||||
heroes.push(postgreShellBtn);
|
heroes.push(postgreShellBtn);
|
||||||
} else {
|
} else {
|
||||||
@@ -347,10 +355,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
|
|
||||||
const connectBtn = {
|
const connectBtn = {
|
||||||
iconSrc: ConnectIcon,
|
iconSrc: ConnectIcon,
|
||||||
title: userContext.apiType === "Postgres" ? "Connect with PG Admin" : "Connect",
|
title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect",
|
||||||
description:
|
description:
|
||||||
userContext.apiType === "Postgres"
|
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",
|
: "Prefer using your own choice of tooling? Find the connection string you need to connect",
|
||||||
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
|
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
|
||||||
};
|
};
|
||||||
@@ -520,7 +528,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
<Image src={LinkIcon} />
|
<Image src={LinkIcon} alt=" " />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text>{item.description}</Text>
|
<Text>{item.description}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ export default class QueryBuilderViewModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => {
|
public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => {
|
||||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
if (event.key === "Enter" || event.key === "Space") {
|
||||||
this.addClauseIndex(this.clauseArray().length - 1);
|
this.addClauseIndex(this.clauseArray().length - 1);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ You can enable or disable public IP addresses on the worker nodes on 'Networking
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Label>Secure connections</Label>
|
<Label>Secure connections</Label>
|
||||||
<Text>
|
<Text style={{ marginBottom: 8 }}>
|
||||||
Only secure connections are supported. For production use cases, we recommend using the 'verify-full'
|
Only secure connections are supported. For production use cases, we recommend using the 'verify-full'
|
||||||
mode to enforce TLS certificate verification. You will need to download the Hyperscale (Citus) certificate, and
|
mode to enforce TLS certificate verification. You will need to download the Hyperscale (Citus) certificate, and
|
||||||
provide it when connecting to the database.{" "}
|
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
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,24 +70,19 @@
|
|||||||
<tbody data-bind="template: { name: 'queryClause-template', foreach: clauseArray, as: 'clause' }"></tbody>
|
<tbody data-bind="template: { name: 'queryClause-template', foreach: clauseArray, as: 'clause' }"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button
|
||||||
class="addClause"
|
data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }"
|
||||||
role="button"
|
style="border: none; background: none"
|
||||||
data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }, attr: { title: addNewClauseLine }"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
|
<div class="addClause" data-bind=" ">
|
||||||
<div class="addClause-heading">
|
<div class="addClause-heading">
|
||||||
<span class="clause-table addClause-title">
|
<span class="clause-table addClause-title">
|
||||||
<img
|
<img class="addclauseProperty-Img" style="margin-bottom: 5px" src="/Add-property.svg" />
|
||||||
class="addclauseProperty-Img"
|
|
||||||
style="margin-bottom: 5px"
|
|
||||||
src="/Add-property.svg"
|
|
||||||
alt="Add new clause"
|
|
||||||
/>
|
|
||||||
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
|
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Tables Query Tab Query Helper - End-->
|
<!-- Tables Query Tab Query Helper - End-->
|
||||||
@@ -168,22 +163,20 @@
|
|||||||
<script type="text/html" id="queryClause-template">
|
<script type="text/html" id="queryClause-template">
|
||||||
<tr class="clause-table-row">
|
<tr class="clause-table-row">
|
||||||
<td class="clause-table-cell action-column">
|
<td class="clause-table-cell action-column">
|
||||||
<span
|
<button
|
||||||
class="entity-Add-Cancel"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="click: $parent.addClauseIndex.bind($data, $index()), event: { keydown: $parent.onAddClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.insertNewFilterLine}"
|
data-bind="click: $parent.addClauseIndex.bind($data, $index()), event: { keydown: $parent.onAddClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.insertNewFilterLine}"
|
||||||
>
|
>
|
||||||
|
<span class="entity-Add-Cancel" role="button">
|
||||||
<img class="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" />
|
<img class="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
</button>
|
||||||
class="entity-Add-Cancel"
|
<button
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="hasFocus: isDeleteButtonFocused, click: $parent.deleteClause.bind($data, $index()), event: { keydown: $parent.onDeleteClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.removeThisFilterLine}"
|
data-bind="hasFocus: isDeleteButtonFocused, click: $parent.deleteClause.bind($data, $index()), event: { keydown: $parent.onDeleteClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.removeThisFilterLine}"
|
||||||
>
|
>
|
||||||
|
<span class="entity-Add-Cancel" role="button">
|
||||||
<img class="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" />
|
<img class="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" />
|
||||||
</span>
|
</span>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="clause-table-cell group-control-column">
|
<td class="clause-table-cell group-control-column">
|
||||||
<input type="checkbox" aria-label="And/Or" data-bind="checked: checkedForGrouping" />
|
<input type="checkbox" aria-label="And/Or" data-bind="checked: checkedForGrouping" />
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
|
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
import { configContext } from "ConfigContext";
|
||||||
|
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
|
||||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||||
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
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 { userContext } from "UserContext";
|
||||||
|
import { armRequest } from "Utils/arm/request";
|
||||||
|
|
||||||
interface QuickstartTabProps {
|
interface QuickstartTabProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -13,14 +17,42 @@ interface QuickstartTabProps {
|
|||||||
|
|
||||||
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
|
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
|
||||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
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(() => {
|
useEffect(() => {
|
||||||
explorer.allocateContainer();
|
explorer.allocateContainer();
|
||||||
}, []);
|
}, []);
|
||||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
|
||||||
authToken: notebookServerInfo.authToken,
|
|
||||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`,
|
|
||||||
forwardingId: notebookServerInfo.forwardingId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ width: "100%" }} horizontal>
|
<Stack style={{ width: "100%" }} horizontal>
|
||||||
@@ -28,15 +60,24 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
|||||||
<QuickstartGuide />
|
<QuickstartGuide />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||||
{notebookServerInfo?.notebookServerEndpoint && (
|
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
|
||||||
|
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||||
<NotebookTerminalComponent
|
<NotebookTerminalComponent
|
||||||
notebookServerInfo={getNotebookServerInfo()}
|
notebookServerInfo={getNotebookServerInfo()}
|
||||||
databaseAccount={userContext.databaseAccount}
|
databaseAccount={userContext.databaseAccount}
|
||||||
tabId="EmbbedTerminal"
|
tabId="QuickstartPSQLShell"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!notebookServerInfo?.notebookServerEndpoint && (
|
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
|
||||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
<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>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -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 { 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";
|
||||||
@@ -21,10 +24,23 @@ interface TabsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<div className="tabsManagerContainer">
|
||||||
|
{networkSettingsWarning && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={MessageBarType.warning}
|
||||||
|
actions={
|
||||||
|
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}>
|
||||||
|
Change network settings
|
||||||
|
</MessageBarButton>
|
||||||
|
}
|
||||||
|
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||||
|
>
|
||||||
|
{networkSettingsWarning}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
<div id="content" className="flexContainer hideOverflows">
|
<div id="content" className="flexContainer hideOverflows">
|
||||||
<div className="nav-tabs-margin">
|
<div className="nav-tabs-margin">
|
||||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||||
|
import { configContext } from "ConfigContext";
|
||||||
|
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { armRequest } from "Utils/arm/request";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -26,10 +29,15 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
constructor(
|
constructor(
|
||||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||||
private getTabId: () => string
|
private getTabId: () => string,
|
||||||
|
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
|
if (!this.isAllPublicIPAddressesEnabled()) {
|
||||||
|
return <QuickstartFirewallNotification />;
|
||||||
|
}
|
||||||
|
|
||||||
return this.parameters() ? (
|
return this.parameters() ? (
|
||||||
<NotebookTerminalComponent
|
<NotebookTerminalComponent
|
||||||
notebookServerInfo={this.getNotebookServerInfo()}
|
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> ';
|
public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> ';
|
||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;
|
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;
|
||||||
|
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
||||||
|
|
||||||
constructor(options: TerminalTabOptions) {
|
constructor(options: TerminalTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
|
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||||
() => this.getNotebookServerInfo(options),
|
() => this.getNotebookServerInfo(options),
|
||||||
() => userContext?.databaseAccount,
|
() => userContext?.databaseAccount,
|
||||||
() => this.tabId
|
() => this.tabId,
|
||||||
|
this.isAllPublicIPAddressesEnabled
|
||||||
);
|
);
|
||||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
if (
|
if (
|
||||||
this.isTemplateReady() &&
|
this.isTemplateReady() &&
|
||||||
useNotebook.getState().isNotebookEnabled &&
|
useNotebook.getState().isNotebookEnabled &&
|
||||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint
|
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
|
||||||
|
this.isAllPublicIPAddressesEnabled()
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
||||||
|
this.checkPostgresFirewallRules();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
@@ -110,4 +126,25 @@ export default class TerminalTab extends TabsBase {
|
|||||||
forwardingId: info.forwardingId,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1160,23 +1160,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.onDocumentDBDocumentsClick();
|
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 {
|
public getDatabase(): ViewModels.Database {
|
||||||
return useDatabases.getState().findDatabaseWithId(this.databaseId);
|
return useDatabases.getState().findDatabaseWithId(this.databaseId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import shallow from "zustand/shallow";
|
import shallow from "zustand/shallow";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import DeleteIcon from "../../../images/delete.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 buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
|
||||||
const children: TreeNode[] = [];
|
const children: TreeNode[] = [];
|
||||||
children.push({
|
children.push({
|
||||||
label: collection.getLabel(),
|
label: getItemName(),
|
||||||
id: collection.isSampleCollection ? "sampleItems" : "",
|
id: collection.isSampleCollection ? "sampleItems" : "",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import DeleteIcon from "../../../images/delete.svg";
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
import GalleryIcon from "../../../images/GalleryIcon.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 {
|
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
|
||||||
const children: TreeNode[] = [];
|
const children: TreeNode[] = [];
|
||||||
children.push({
|
children.push({
|
||||||
label: collection.getLabel(),
|
label: getItemName(),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
// push to most recent
|
// push to most recent
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ export class PhoenixClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
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>> {
|
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||||
@@ -80,9 +84,12 @@ export class PhoenixClient {
|
|||||||
}
|
}
|
||||||
const phoenixError = responseJson as IPhoenixError;
|
const phoenixError = responseJson as IPhoenixError;
|
||||||
if (response.status === HttpStatusCodes.Forbidden) {
|
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 Error(phoenixError.message);
|
throw new AbortError(this.ConvertToForbiddenErrorString(phoenixError));
|
||||||
|
}
|
||||||
|
throw new AbortError(phoenixError.message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error.status = response?.status;
|
error.status = response?.status;
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,7 @@ export type Features = {
|
|||||||
readonly mongoProxyEndpoint?: string;
|
readonly mongoProxyEndpoint?: string;
|
||||||
readonly mongoProxyAPIs?: string;
|
readonly mongoProxyAPIs?: string;
|
||||||
readonly enableThroughputCap: boolean;
|
readonly enableThroughputCap: boolean;
|
||||||
|
readonly enableHierarchicalKeys: boolean;
|
||||||
|
|
||||||
// can be set via both flight and feature flag
|
// can be set via both flight and feature flag
|
||||||
autoscaleDefault: boolean;
|
autoscaleDefault: boolean;
|
||||||
@@ -90,6 +91,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
||||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||||
|
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -81,6 +81,9 @@ export class JupyterLabAppFactory {
|
|||||||
// Attach the widget to the dom.
|
// Attach the widget to the dom.
|
||||||
Widget.attach(panel, document.body);
|
Widget.attach(panel, document.body);
|
||||||
|
|
||||||
|
// Switch focus to the terminal
|
||||||
|
term.activate();
|
||||||
|
|
||||||
// Handle resize events.
|
// Handle resize events.
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
panel.update();
|
panel.update();
|
||||||
@@ -90,5 +93,7 @@ export class JupyterLabAppFactory {
|
|||||||
window.addEventListener("unload", () => {
|
window.addEventListener("unload", () => {
|
||||||
panel.dispose();
|
panel.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ interface UserContext {
|
|||||||
partitionKey?: string;
|
partitionKey?: string;
|
||||||
};
|
};
|
||||||
readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
|
readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
|
||||||
|
readonly isReplica?: boolean;
|
||||||
collectionCreationDefaults: CollectionCreationDefaults;
|
collectionCreationDefaults: CollectionCreationDefaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,16 +110,19 @@ function updateUserContext(newContext: Partial<UserContext>): void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!localStorage.getItem(newContext.databaseAccount.id)) {
|
if (!localStorage.getItem(newContext.databaseAccount.id)) {
|
||||||
if (newContext.apiType === "Postgres") {
|
if (newContext.isTryCosmosDBSubscription || isNewAccount) {
|
||||||
usePostgres.getState().setShowPostgreTeachingBubble(true);
|
if (newContext.apiType === "Postgres" && !newContext.isReplica) {
|
||||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
|
||||||
}
|
|
||||||
if (userContext.isTryCosmosDBSubscription || isNewAccount) {
|
|
||||||
useCarousel.getState().setShouldOpen(true);
|
|
||||||
usePostgres.getState().setShowResetPasswordBubble(true);
|
usePostgres.getState().setShowResetPasswordBubble(true);
|
||||||
|
usePostgres.getState().setShowPostgreTeachingBubble(true);
|
||||||
|
} else {
|
||||||
|
useCarousel.getState().setShouldOpen(true);
|
||||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||||
traceOpen(Action.OpenCarousel);
|
traceOpen(Action.OpenCarousel);
|
||||||
}
|
}
|
||||||
|
} else if (newContext.apiType === "Postgres") {
|
||||||
|
usePostgres.getState().setShowPostgreTeachingBubble(true);
|
||||||
|
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.assign(userContext, newContext);
|
Object.assign(userContext, newContext);
|
||||||
|
|||||||
@@ -74,3 +74,18 @@ export const getApiShortDisplayName = (): string => {
|
|||||||
return "Table API";
|
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";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
52
src/Utils/NetworkUtility.ts
Normal file
52
src/Utils/NetworkUtility.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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 getNetworkSettingsWarningMessage = (clientIpAddress: string): string => {
|
||||||
|
const accountProperties = userContext.databaseAccount?.properties;
|
||||||
|
|
||||||
|
if (!accountProperties) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// public network access is disabled
|
||||||
|
if (accountProperties.publicNetworkAccess !== "Enabled") {
|
||||||
|
return "The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipRules = accountProperties.ipRules;
|
||||||
|
// public network access is set to "All networks"
|
||||||
|
if (ipRules.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") {
|
||||||
|
const portalIPs = PortalIPs[userContext.portalEnv];
|
||||||
|
let numberOfMatches = 0;
|
||||||
|
ipRules.forEach((ipRule) => {
|
||||||
|
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
|
||||||
|
numberOfMatches++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (numberOfMatches !== portalIPs.length) {
|
||||||
|
return "The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
if (!clientIpAddress || ipRules.some((ipRule) => ipRule.ipAddressOrRange === clientIpAddress)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "The Network settings for this account are preventing access from Data Explorer. Please add your current IP to the firewall rules to proceed.";
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { AccountKind, Flights } from "../Common/Constants";
|
import { AccountKind, Flights } from "../Common/Constants";
|
||||||
@@ -100,8 +101,12 @@ async function configureHosted(): Promise<Explorer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.data?.type === MessageTypes.CloseTab) {
|
if (event.data?.type === MessageTypes.CloseTab) {
|
||||||
|
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
||||||
|
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||||
|
} else {
|
||||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -290,8 +295,12 @@ 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) {
|
||||||
|
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
||||||
|
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||||
|
} else {
|
||||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@@ -356,7 +365,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (inputs.isPostgresAccount) {
|
if (inputs.isPostgresAccount) {
|
||||||
updateUserContext({ apiType: "Postgres" });
|
updateUserContext({ apiType: "Postgres", isReplica: !!inputs.isReplica });
|
||||||
|
|
||||||
if (inputs.connectionStringParams) {
|
if (inputs.connectionStringParams) {
|
||||||
// TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension
|
// 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 warningMessage = getNetworkSettingsWarningMessage(inputs.clientIpAddress);
|
||||||
|
useTabs.getState().setNetworkSettingsWarning(warningMessage);
|
||||||
|
|
||||||
if (inputs.features) {
|
if (inputs.features) {
|
||||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface TabsState {
|
|||||||
openedReactTabs: ReactTabKind[];
|
openedReactTabs: ReactTabKind[];
|
||||||
activeTab: TabsBase | undefined;
|
activeTab: TabsBase | undefined;
|
||||||
activeReactTab: ReactTabKind | undefined;
|
activeReactTab: ReactTabKind | undefined;
|
||||||
|
networkSettingsWarning: string;
|
||||||
activateTab: (tab: TabsBase) => void;
|
activateTab: (tab: TabsBase) => void;
|
||||||
activateNewTab: (tab: TabsBase) => void;
|
activateNewTab: (tab: TabsBase) => void;
|
||||||
activateReactTab: (tabkind: ReactTabKind) => void;
|
activateReactTab: (tabkind: ReactTabKind) => void;
|
||||||
@@ -20,6 +21,7 @@ interface TabsState {
|
|||||||
closeAllNotebookTabs: (hardClose: boolean) => void;
|
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||||
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
||||||
closeReactTab: (tabKind: ReactTabKind) => void;
|
closeReactTab: (tabKind: ReactTabKind) => void;
|
||||||
|
setNetworkSettingsWarning: (warningMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReactTabKind {
|
export enum ReactTabKind {
|
||||||
@@ -33,6 +35,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
openedReactTabs: [ReactTabKind.Home],
|
openedReactTabs: [ReactTabKind.Home],
|
||||||
activeTab: undefined,
|
activeTab: undefined,
|
||||||
activeReactTab: ReactTabKind.Home,
|
activeReactTab: ReactTabKind.Home,
|
||||||
|
networkSettingsWarning: "",
|
||||||
activateTab: (tab: TabsBase): void => {
|
activateTab: (tab: TabsBase): void => {
|
||||||
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
||||||
set({ activeTab: tab, activeReactTab: undefined });
|
set({ activeTab: tab, activeReactTab: undefined });
|
||||||
@@ -142,4 +145,5 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
|
|
||||||
set({ openedReactTabs: updatedOpenedReactTabs });
|
set({ openedReactTabs: updatedOpenedReactTabs });
|
||||||
},
|
},
|
||||||
|
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
26
src/hooks/useTerminal.ts
Normal file
26
src/hooks/useTerminal.ts
Normal 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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -82,7 +82,6 @@
|
|||||||
"./src/Explorer/Tree/AccessibleVerticalList.ts",
|
"./src/Explorer/Tree/AccessibleVerticalList.ts",
|
||||||
"./src/GitHub/GitHubConnector.ts",
|
"./src/GitHub/GitHubConnector.ts",
|
||||||
"./src/HostedExplorerChildFrame.ts",
|
"./src/HostedExplorerChildFrame.ts",
|
||||||
"./src/Platform/Hosted/Authorization.ts",
|
|
||||||
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
||||||
"./src/Platform/Hosted/Components/MeControl.tsx",
|
"./src/Platform/Hosted/Components/MeControl.tsx",
|
||||||
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
||||||
@@ -126,7 +125,6 @@
|
|||||||
"./src/Utils/WindowUtils.ts",
|
"./src/Utils/WindowUtils.ts",
|
||||||
"./src/hooks/useConfig.ts",
|
"./src/hooks/useConfig.ts",
|
||||||
"./src/hooks/useDirectories.tsx",
|
"./src/hooks/useDirectories.tsx",
|
||||||
"./src/hooks/useFullScreenURLs.tsx",
|
|
||||||
"./src/hooks/useGraphPhoto.tsx",
|
"./src/hooks/useGraphPhoto.tsx",
|
||||||
"./src/hooks/useNotebookSnapshotStore.ts",
|
"./src/hooks/useNotebookSnapshotStore.ts",
|
||||||
"./src/hooks/usePortalAccessToken.tsx",
|
"./src/hooks/usePortalAccessToken.tsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user