Compare commits

...

19 Commits

Author SHA1 Message Date
Sampath
3ada04da73 heading role along with level of heading added to the element for the screen reader 2024-02-13 23:46:43 +05:30
JustinKol
e43b4eee5c Correcting import for MessageTypes (#1743) 2024-02-13 10:35:14 -05:00
Asier Isayas
8b0b3b07d6 Add Mongo Proxy to Data Explorer (Standalone and Portal) (#1738)
* Mongo Proxy backend API

* merge main into current

* allow mongo proxy endpoints to be constants

* allow mongo proxy endpoints to be constants

* fix test

* check for allowed mongo proxy endpoint

* check for allowed mongo proxy endpoint

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-02-09 10:58:10 -05:00
Laurent Nguyen
f403b086ad Hide buttons for Fabric or when no write access (#1742) 2024-02-09 15:10:57 +01:00
jawelton74
f8cfc6c21c Remove references to addCollectionDefaultFlight parameter. (#1741) 2024-02-08 11:49:40 -08:00
Asier Isayas
35ca7944ae Limit RU threshold only to NoSQL (#1739)
* limit RU threshold only to NoSQL

* limit RU threshold only to NoSQL

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-02-07 11:31:13 -05:00
sindhuba
6d98b4a500 Remove localStorage for NPS (#1733)
* Remove localStorage for NPS

* Run npm format

* Update comment
2024-02-06 09:54:50 -08:00
JustinKol
2d06eef9cc Added CESCVA MessageType for FE (#1737) 2024-02-06 12:46:25 -05:00
Asier Isayas
31f7178669 Change RU Threshold to 5000 (#1735)
* ru threshold beta

* use new ru threshold package

* fix typo

* fix merge issue

* fix package-lock.json

* fix test

* fixed settings pane test

* fixed merge issue

* sync with main

* fixed settings pane check

* fix checks

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* remove learn more

* change default RU threshold to 5000

* change default RU threshold to 5000

* change default RU threshold to 5000

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-02-02 11:52:37 -05:00
JustinKol
c0b54f6e84 Added TableAPI whitespace check and trim values (#1731)
* Added TableAPI whitespace check and trim values

* Reverted value is not required unless Row or PrimaryKey

* Prettier Run

* Add full whitespace check on Keys

* Prettier formatting

* Fixed type

* Added whitespace check for tabs, vertical tabs, formfeeds, line breaks, etc

* Fixed logic
2024-02-01 12:51:23 -05:00
jawelton74
cd3eb5b5b3 Fix regression in Quickstart workflow. (#1732) 2024-01-30 16:31:46 -08:00
Asier Isayas
dbb0324a64 RU Threshold (#1728)
* ru threshold beta

* use new ru threshold package

* fix typo

* fix merge issue

* fix package-lock.json

* fix test

* fixed settings pane test

* fixed merge issue

* sync with main

* fixed settings pane check

* fix checks

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* remove learn more

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-01-30 16:21:29 -05:00
sindhuba
e207f3702b Add more logs for NPS (#1729) 2024-01-24 16:46:28 -08:00
MokireddySampath
f496220ed6 Empty coumnheader has been given the icon with name of description (#1718) 2024-01-23 13:28:13 +05:30
MokireddySampath
eb790d09b5 role of heading has been added to the text that is visually appearing… (#1701)
* role of heading has been added to the text that is visually appearing as heading

* Update WelcomeModal.test.tsx.snap
2024-01-23 00:08:27 +05:30
MokireddySampath
323305e485 state of the buttons will now be updated by screen reader (#1716) 2024-01-20 09:17:47 +05:30
sunghyunkang1111
70635e426f Fix the teaching bubble popup and enable copilot card (#1722)
* Fix the teaching bubble popup and enable copilot card

* add close copilot button title

* fix compilation
2024-01-19 09:37:17 -06:00
sindhuba
5a5bf34d4d Update logic for NPS survey for existing accounts > 90 days (#1725)
* Update logic for NPS survey for existing accounts > 90 days

* Remove lint error

* Address comments

* Fix error in code
2024-01-19 07:08:11 -08:00
Laurent Nguyen
0975591945 Fabric: clean up RPC and support show/hide toolbar message (#1680)
* Use Promise for allResourceToken fabric message. Cleanup token message handling and add debounce.

* Improve rpc and update initalization flow

* Fix format

* Rev up message names for new version

* Refactor RPC with Fabric

* Build fix

* Fix build

* Fix format

* Update Message types

* Fix format

* Fix comments

* Fabric toolbar style and support to show/hide it (#1720)

* Add Fabric specific Toolbar design

* Add Fabric message to show/hide the Toolbar

* Fix CommandBarUtil formatting

* Update zustand state on setToolbarStatus to trigger a redraw of the command bar with updated visibility

---------

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* Fix format

---------

Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com>
2024-01-18 17:13:04 +01:00
57 changed files with 995 additions and 273 deletions

View File

@@ -145,4 +145,5 @@ src/Explorer/Notebook/temp/inputs/connected-editors/codemirror.tsx
src/Explorer/Tree/ResourceTreeAdapter.tsx src/Explorer/Tree/ResourceTreeAdapter.tsx
__mocks__/monaco-editor.ts __mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTree.tsx src/Explorer/Tree/ResourceTree.tsx
src/Utils/EndpointUtils.ts
src/Utils/PriorityBasedExecutionUtils.ts src/Utils/PriorityBasedExecutionUtils.ts

View File

@@ -147,6 +147,7 @@
// CommandBar // CommandBar
@CommandBarButtonHeight: 40px; @CommandBarButtonHeight: 40px;
@FabricCommandBarButtonHeight: 34px;
/********************************************************************************** /**********************************************************************************
Portal Consts Portal Consts
@@ -164,7 +165,7 @@
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; @FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
@FabricBoxBorderRadius: 8px; @FabricBoxBorderRadius: 8px;
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14); @FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
@FabricBoxMargin: 4px 3px 4px 3px; @FabricBoxMargin: 4px 3px 4px 3px;
@FabricAccentMediumHigh: #0c695a; @FabricAccentMediumHigh: #0c695a;

View File

@@ -47,11 +47,15 @@ a:focus {
border-radius: @FabricBoxBorderRadius; border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow; box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin; margin: @FabricBoxMargin;
margin-top: 0px;
padding-top: 2px; padding-top: 2px;
padding: 0px;
height: 40px;
} }
.dividerContainer { .dividerContainer {
padding: @SmallSpace 0px @SmallSpace 0px; padding: @SmallSpace 0px @SmallSpace 0px;
height: @FabricCommandBarButtonHeight;
.flex-display(); .flex-display();
span { span {

18
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.0", "@azure/cosmos": "4.0.1-beta.2",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.0.7",
@@ -396,9 +396,9 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
}, },
"node_modules/@azure/cosmos": { "node_modules/@azure/cosmos": {
"version": "4.0.0", "version": "4.0.1-beta.2",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.2.tgz",
"integrity": "sha512-/Z27p1+FTkmjmm8jk90zi/HrczPHw2t8WecFnsnTe4xGocWl0Z4clP0YlLUTJPhRLWYa5upwD9rMvKJkS1f1kg==", "integrity": "sha512-iuqg/QwLQlxgRi4pnXU8JUYv+f24wkRvJ9ZZI4/sYk+DxSgkuQ194Cc2IpckpeO8z7ZpcBkVQFa82wcZVVZ8Zg==",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^1.0.0", "@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.3.0", "@azure/core-auth": "^1.3.0",
@@ -408,14 +408,14 @@
"fast-json-stable-stringify": "^2.1.0", "fast-json-stable-stringify": "^2.1.0",
"jsbi": "^3.1.3", "jsbi": "^3.1.3",
"node-abort-controller": "^3.0.0", "node-abort-controller": "^3.0.0",
"priorityqueuejs": "^1.0.0", "priorityqueuejs": "^2.0.0",
"semaphore": "^1.0.5", "semaphore": "^1.0.5",
"tslib": "^2.2.0", "tslib": "^2.2.0",
"universal-user-agent": "^6.0.0", "universal-user-agent": "^6.0.0",
"uuid": "^8.3.0" "uuid": "^8.3.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@azure/cosmos-language-service": { "node_modules/@azure/cosmos-language-service": {
@@ -33707,9 +33707,9 @@
} }
}, },
"node_modules/priorityqueuejs": { "node_modules/priorityqueuejs": {
"version": "1.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz",
"integrity": "sha512-lg++21mreCEOuGWTbO5DnJKAdxfjrdN0S9ysoW9SzdSJvbkWpkaDdpG/cdsPCsEnoLUwmd9m3WcZhngW7yKA2g==" "integrity": "sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ=="
}, },
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.29.0", "version": "1.29.0",

View File

@@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.0", "@azure/cosmos": "4.0.1-beta.2",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.0.7",

View File

@@ -211,6 +211,10 @@ export class HttpHeaders {
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
} }
export class ContentType {
public static applicationJson: string = "application/json";
}
export class ApiType { export class ApiType {
// Mapped to hexadecimal values in the backend // Mapped to hexadecimal values in the backend
public static readonly MongoDB: number = 1; public static readonly MongoDB: number = 1;

View File

@@ -40,9 +40,10 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
case Cosmos.ResourceType.item: case Cosmos.ResourceType.item:
case Cosmos.ResourceType.pkranges: case Cosmos.ResourceType.pkranges:
// User resource tokens // User resource tokens
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
headers[HttpHeaders.msDate] = new Date().toUTCString(); headers[HttpHeaders.msDate] = new Date().toUTCString();
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens; const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp); checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId); return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
case Cosmos.ResourceType.none: case Cosmos.ResourceType.none:
@@ -51,9 +52,11 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
case Cosmos.ResourceType.user: case Cosmos.ResourceType.user:
case Cosmos.ResourceType.permission: case Cosmos.ResourceType.permission:
// User master tokens // User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [ const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
requestInfo, MessageTypes.GetAuthorizationToken,
]); [requestInfo],
userContext.fabricContext.connectionId,
);
console.log("Response from Fabric: ", authorizationToken); console.log("Response from Fabric: ", authorizationToken);
headers[HttpHeaders.msDate] = authorizationToken.XDate; headers[HttpHeaders.msDate] = authorizationToken.XDate;
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken); return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);

View File

@@ -1,3 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../Contracts/ViewModels"; import { QueryResults } from "../Contracts/ViewModels";
interface QueryResponse { interface QueryResponse {
@@ -10,13 +11,17 @@ interface QueryResponse {
} }
export interface MinimalQueryIterator { export interface MinimalQueryIterator {
fetchNext: () => Promise<QueryResponse>; fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
} }
// Pick<QueryIterator<any>, "fetchNext">; // Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(
return documentsIterator.fetchNext().then((response) => { documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
const documents = response.resources; const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any

View File

@@ -27,15 +27,24 @@ export function handleCachedDataMessage(message: any): void {
runGarbageCollector(); runGarbageCollector();
} }
/**
*
* @param messageType
* @param params
* @param scope Use this string to identify request Useful to distinguish response from different senders
* @param timeoutInMs
* @returns
*/
export function sendCachedDataMessage<TResponseDataModel>( export function sendCachedDataMessage<TResponseDataModel>(
messageType: MessageTypes, messageType: MessageTypes,
params: Object[], params: Object[],
scope?: string,
timeoutInMs?: number, timeoutInMs?: number,
): Q.Promise<TResponseDataModel> { ): Q.Promise<TResponseDataModel> {
let cachedDataPromise: CachedDataPromise<TResponseDataModel> = { let cachedDataPromise: CachedDataPromise<TResponseDataModel> = {
deferred: Q.defer<TResponseDataModel>(), deferred: Q.defer<TResponseDataModel>(),
startTime: new Date(), startTime: new Date(),
id: _.uniqueId(), id: _.uniqueId(scope),
}; };
RequestMap[cachedDataPromise.id] = cachedDataPromise; RequestMap[cachedDataPromise.id] = cachedDataPromise;
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id }); sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
@@ -47,6 +56,10 @@ export function sendCachedDataMessage<TResponseDataModel>(
); );
} }
/**
*
* @param data Overwrite the data property of the message
*/
export function sendMessage(data: any): void { export function sendMessage(data: any): void {
_sendMessage({ _sendMessage({
signature: "pcIframe", signature: "pcIframe",

View File

@@ -1,6 +1,6 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { MongoProxyEndpoints, allowedMongoProxyEndpoints_ToBeDeprecated, validateEndpoint } from "Utils/EndpointUtils";
import queryString from "querystring"; import queryString from "querystring";
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
@@ -10,7 +10,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures"; import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -62,6 +62,73 @@ export function queryDocuments(
isResourceList: boolean, isResourceList: boolean,
query: string, query: string,
continuationToken?: string, continuationToken?: string,
): Promise<QueryResponse> {
if (!useMongoProxyEndpoint("resourcelist")) {
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
}
const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = {
databaseID: databaseId,
collectionID: collection.id(),
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
resourceID: collection.rid,
resourceType: "docs",
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
partitionKey:
collection && collection.partitionKey && !collection.partitionKey.systemKey
? collection.partitionKeyProperties?.[0]
: "",
query,
};
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
const headers = {
...defaultHeaders,
...authHeaders(),
[CosmosSDKConstants.HttpHeaders.IsQuery]: "true",
[CosmosSDKConstants.HttpHeaders.PopulateQueryMetrics]: "true",
[CosmosSDKConstants.HttpHeaders.EnableScanInQuery]: "true",
[CosmosSDKConstants.HttpHeaders.EnableCrossPartitionQuery]: "true",
[CosmosSDKConstants.HttpHeaders.ParallelizeCrossPartitionQuery]: "true",
[HttpHeaders.contentType]: "application/query+json",
};
if (continuationToken) {
headers[CosmosSDKConstants.HttpHeaders.Continuation] = continuationToken;
}
const path = isResourceList ? "/resourcelist" : "";
return window
.fetch(`${endpoint}${path}`, {
method: "POST",
body: JSON.stringify(params),
headers,
})
.then(async (response) => {
if (response.ok) {
return {
continuationToken: response.headers.get(CosmosSDKConstants.HttpHeaders.Continuation),
documents: (await response.json()).Documents as DataModels.DocumentId[],
headers: response.headers,
};
}
await errorHandling(response, "querying documents", params);
return undefined;
});
}
function queryDocuments_ToBeDeprecated(
databaseId: string,
collection: Collection,
isResourceList: boolean,
query: string,
continuationToken?: string,
): Promise<QueryResponse> { ): Promise<QueryResponse> {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
@@ -122,6 +189,54 @@ export function readDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
documentId: DocumentId, documentId: DocumentId,
): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint("readDocument")) {
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
}
const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 4).join("/");
const rid = encodeURIComponent(idComponents[5]);
const params = {
databaseID: databaseId,
collectionID: collection.id(),
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
resourceID: rid,
resourceType: "docs",
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
partitionKey:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
: "",
};
const endpoint = getFeatureEndpointOrDefault("readDocument");
return window
.fetch(endpoint, {
method: "POST",
body: JSON.stringify(params),
headers: {
...defaultHeaders,
...authHeaders(),
[HttpHeaders.contentType]: ContentType.applicationJson,
},
})
.then(async (response) => {
if (response.ok) {
return response.json();
}
return await errorHandling(response, "reading document", params);
});
}
export function readDocument_ToBeDeprecated(
databaseId: string,
collection: Collection,
documentId: DocumentId,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
@@ -169,6 +284,51 @@ export function createDocument(
collection: Collection, collection: Collection,
partitionKeyProperty: string, partitionKeyProperty: string,
documentContent: unknown, documentContent: unknown,
): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint("createDocument")) {
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
}
const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = {
databaseID: databaseId,
collectionID: collection.id(),
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
resourceID: collection.rid,
resourceType: "docs",
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
partitionKey:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
documentContent: JSON.stringify(documentContent),
};
const endpoint = getFeatureEndpointOrDefault("createDocument");
return window
.fetch(`${endpoint}/createDocument`, {
method: "POST",
body: JSON.stringify(params),
headers: {
...defaultHeaders,
...authHeaders(),
[HttpHeaders.contentType]: ContentType.applicationJson,
},
})
.then(async (response) => {
if (response.ok) {
return response.json();
}
return await errorHandling(response, "creating document", params);
});
}
export function createDocument_ToBeDeprecated(
databaseId: string,
collection: Collection,
partitionKeyProperty: string,
documentContent: unknown,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
@@ -208,6 +368,56 @@ export function updateDocument(
collection: Collection, collection: Collection,
documentId: DocumentId, documentId: DocumentId,
documentContent: string, documentContent: string,
): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint("updateDocument")) {
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
}
const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/");
const rid = encodeURIComponent(idComponents[5]);
const params = {
databaseID: databaseId,
collectionID: collection.id(),
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
resourceID: rid,
resourceType: "docs",
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
partitionKey:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
: "",
documentContent,
};
const endpoint = getFeatureEndpointOrDefault("updateDocument");
return window
.fetch(endpoint, {
method: "PUT",
body: JSON.stringify(params),
headers: {
...defaultHeaders,
...authHeaders(),
[HttpHeaders.contentType]: ContentType.applicationJson,
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
},
})
.then(async (response) => {
if (response.ok) {
return response.json();
}
return await errorHandling(response, "updating document", params);
});
}
export function updateDocument_ToBeDeprecated(
databaseId: string,
collection: Collection,
documentId: DocumentId,
documentContent: string,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
@@ -237,7 +447,7 @@ export function updateDocument(
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders(),
[HttpHeaders.contentType]: "application/json", [HttpHeaders.contentType]: ContentType.applicationJson,
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
}, },
}) })
@@ -250,6 +460,53 @@ export function updateDocument(
} }
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> { export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
if (!useMongoProxyEndpoint("deleteDocument")) {
deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
}
const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/");
const path = idComponents.slice(0, 5).join("/");
const rid = encodeURIComponent(idComponents[5]);
const params = {
databaseID: databaseId,
collectionID: collection.id(),
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
resourceID: rid,
resourceType: "docs",
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
partitionKey:
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
: "",
};
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
return window
.fetch(endpoint, {
method: "DELETE",
body: JSON.stringify(params),
headers: {
...defaultHeaders,
...authHeaders(),
[HttpHeaders.contentType]: ContentType.applicationJson,
},
})
.then(async (response) => {
if (response.ok) {
return undefined;
}
return await errorHandling(response, "deleting document", params);
});
}
export function deleteDocument_ToBeDeprecated(
databaseId: string,
collection: Collection,
documentId: DocumentId,
): Promise<void> {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
@@ -277,7 +534,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
...authHeaders(), ...authHeaders(),
[HttpHeaders.contentType]: "application/json", [HttpHeaders.contentType]: ContentType.applicationJson,
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()), [CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
}, },
}) })
@@ -291,6 +548,52 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
export function createMongoCollectionWithProxy( export function createMongoCollectionWithProxy(
params: DataModels.CreateCollectionParams, params: DataModels.CreateCollectionParams,
): Promise<DataModels.Collection> {
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
createMongoCollectionWithProxy_ToBeDeprecated(params);
}
const { databaseAccount } = userContext;
const shardKey: string = params.partitionKey?.paths[0];
const createCollectionParams = {
databaseID: params.databaseId,
collectionID: params.collectionId,
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
resourceID: "",
resourceType: "colls",
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
partitionKey: shardKey,
isAutoscale: !!params.autoPilotMaxThroughput,
hasSharedThroughput: params.databaseLevelThroughput,
offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput,
createDatabase: params.createNewDatabase,
isSharded: !!shardKey,
};
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
return window
.fetch(`${endpoint}/createCollection`, {
method: "POST",
body: JSON.stringify(createCollectionParams),
headers: {
...defaultHeaders,
...authHeaders(),
[HttpHeaders.contentType]: ContentType.applicationJson,
},
})
.then(async (response) => {
if (response.ok) {
return response.json();
}
return await errorHandling(response, "creating collection", createCollectionParams);
});
}
export function createMongoCollectionWithProxy_ToBeDeprecated(
params: DataModels.CreateCollectionParams,
): Promise<DataModels.Collection> { ): Promise<DataModels.Collection> {
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const shardKey: string = params.partitionKey?.paths[0]; const shardKey: string = params.partitionKey?.paths[0];
@@ -334,13 +637,17 @@ export function createMongoCollectionWithProxy(
return await errorHandling(response, "creating collection", mongoParams); return await errorHandling(response, "creating collection", mongoParams);
}); });
} }
export function getFeatureEndpointOrDefault(feature: string): string { export function getFeatureEndpointOrDefault(feature: string): string {
const endpoint = let endpoint;
hasFlag(userContext.features.mongoProxyAPIs, feature) && if (useMongoProxyEndpoint(feature)) {
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints) endpoint = configContext.MONGO_PROXY_ENDPOINT;
? userContext.features.mongoProxyEndpoint } else {
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; endpoint =
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints_ToBeDeprecated)
? userContext.features.mongoProxyEndpoint
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
}
return getEndpoint(endpoint); return getEndpoint(endpoint);
} }
@@ -349,7 +656,11 @@ export function getEndpoint(endpoint: string): string {
let url = endpoint + "/api/mongo/explorer"; let url = endpoint + "/api/mongo/explorer";
if (userContext.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo"); if (endpoint === configContext.MONGO_PROXY_ENDPOINT) {
url = url.replace("api/mongo", "api/connectionstring/mongo");
} else {
url = url.replace("api/mongo", "api/guest/mongo");
}
} }
return url; return url;
} }
@@ -370,3 +681,10 @@ async function errorHandling(response: Response, action: string, params: unknown
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
} }
function useMongoProxyEndpoint(api: string): boolean {
return (
configContext.NEW_MONGO_APIS?.includes(api) &&
[MongoProxyEndpoints.Development, MongoProxyEndpoints.MPAC].includes(configContext.MONGO_PROXY_ENDPOINT)
);
}

View File

@@ -1,3 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../../Contracts/ViewModels"; import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
@@ -8,12 +9,13 @@ export const queryDocumentsPage = async (
resourceName: string, resourceName: string,
documentsIterator: MinimalQueryIterator, documentsIterator: MinimalQueryIterator,
firstItemIndex: number, firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> => { ): Promise<QueryResults> => {
const entityName = getEntityName(); const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try { try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
const itemCount = (result.documents && result.documents.length) || 0; const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result; return result;

View File

@@ -18,13 +18,13 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if ( if (
configContext.platform === Platform.Fabric && configContext.platform === Platform.Fabric &&
userContext.fabricDatabaseConnectionInfo && userContext.fabricContext &&
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
) { ) {
const collections: DataModels.Collection[] = []; const collections: DataModels.Collection[] = [];
const promises: Promise<ContainerResponse>[] = []; const promises: Promise<ContainerResponse>[] = [];
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) { for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
// Dictionary key looks like this: dbs/SampleDB/colls/Container // Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/"); const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1]; const tokenDatabaseId = resourceIdObj[1];

View File

@@ -14,8 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[]; let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`); const clearMessage = logConsoleProgress(`Querying databases`);
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) { if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
const tokensData = userContext.fabricDatabaseConnectionInfo; const tokensData = userContext.fabricContext.databaseConnectionInfo;
const databaseIdsSet = new Set<string>(); // databaseId const databaseIdsSet = new Set<string>(); // databaseId

View File

@@ -7,11 +7,12 @@ import {
allowedHostedExplorerEndpoints, allowedHostedExplorerEndpoints,
allowedJunoOrigins, allowedJunoOrigins,
allowedMongoBackendEndpoints, allowedMongoBackendEndpoints,
allowedMongoProxyEndpoints,
allowedMsalRedirectEndpoints, allowedMsalRedirectEndpoints,
defaultAllowedArmEndpoints, defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints, defaultAllowedBackendEndpoints,
validateEndpoint, validateEndpoint,
} from "Utils/EndpointValidation"; } from "Utils/EndpointUtils";
export enum Platform { export enum Platform {
Portal = "Portal", Portal = "Portal",
@@ -38,6 +39,8 @@ export interface ConfigContext {
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string; BACKEND_ENDPOINT?: string;
MONGO_BACKEND_ENDPOINT?: string; MONGO_BACKEND_ENDPOINT?: string;
MONGO_PROXY_ENDPOINT?: string;
NEW_MONGO_APIS?: string[];
PROXY_PATH?: string; PROXY_PATH?: string;
JUNO_ENDPOINT: string; JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string; GITHUB_CLIENT_ID: string;
@@ -82,6 +85,15 @@ let configContext: Readonly<ConfigContext> = {
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: JunoEndpoints.Prod, JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
MONGO_PROXY_ENDPOINT: "https://cdb-ms-prod-mp.cosmos.azure.com",
NEW_MONGO_APIS: [
// "resourcelist",
// "createDocument",
// "readDocument",
// "updateDocument",
// "deleteDocument",
// "createCollectionWithProxy",
],
isTerminalEnabled: false, isTerminalEnabled: false,
isPhoenixEnabled: false, isPhoenixEnabled: false,
}; };
@@ -127,6 +139,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.BACKEND_ENDPOINT; delete newContext.BACKEND_ENDPOINT;
} }
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, allowedMongoProxyEndpoints)) {
delete newContext.MONGO_PROXY_ENDPOINT;
}
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) { if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
delete newContext.MONGO_BACKEND_ENDPOINT; delete newContext.MONGO_BACKEND_ENDPOINT;
} }

View File

@@ -0,0 +1,42 @@
import { MessageTypes } from "./MessageTypes";
// This is the current version of these messages
export const DATA_EXPLORER_RPC_VERSION = "2";
// Data Explorer to Fabric
// TODO Remove when upgrading to Fabric v2
export type DataExploreMessageV1 =
| "ready"
| {
type: MessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
id: string;
};
// -----------------------------
export type DataExploreMessageV2 =
| {
type: MessageTypes.Ready;
id: string;
params: [string]; // version
}
| {
type: MessageTypes.GetAuthorizationToken;
id: string;
params: GetCosmosTokenMessageOptions[];
}
| {
type: MessageTypes.GetAllResourceTokens;
id: string;
};
export type GetCosmosTokenMessageOptions = {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
resourceId: string;
};

View File

@@ -1,6 +1,6 @@
import { MessageTypes } from "Contracts/MessageTypes";
import * as ActionContracts from "./ActionContracts"; import * as ActionContracts from "./ActionContracts";
import * as Diagnostics from "./Diagnostics"; import * as Diagnostics from "./Diagnostics";
import { MessageTypes } from "./MessageTypes";
import * as Versions from "./Versions"; import * as Versions from "./Versions";
export { ActionContracts, Diagnostics, MessageTypes, Versions }; export { ActionContracts, Diagnostics, MessageTypes, Versions };

View File

@@ -1,6 +1,12 @@
import { AuthorizationToken, MessageTypes } from "./MessageTypes"; import { AuthorizationToken } from "./MessageTypes";
export type FabricMessage = // This is the version of these messages
export const FABRIC_RPC_VERSION = "2";
// Fabric to Data Explorer
// TODO Deprecated. Remove this section once DE is updated
export type FabricMessageV1 =
| { | {
type: "newContainer"; type: "newContainer";
databaseName: string; databaseName: string;
@@ -26,38 +32,52 @@ export type FabricMessage =
| { | {
type: "allResourceTokens"; type: "allResourceTokens";
message: { message: {
id: string;
error: string | undefined;
endpoint: string | undefined; endpoint: string | undefined;
databaseId: string | undefined; databaseId: string | undefined;
resourceTokens: unknown | undefined; resourceTokens: unknown | undefined;
resourceTokensTimestamp: number | undefined; resourceTokensTimestamp: number | undefined;
}; };
}; };
// -----------------------------
export type DataExploreMessage = export type FabricMessageV2 =
| "ready"
| { | {
type: MessageTypes.TelemetryInfo; type: "newContainer";
data: { databaseName: string;
action: "LoadDatabases"; }
actionModifier: "success" | "start"; | {
defaultExperience: "SQL"; type: "initialize";
version: string;
id: string;
message: {
connectionId: string;
}; };
} }
| { | {
type: MessageTypes.GetAuthorizationToken; type: "authorizationToken";
id: string; message: {
params: GetCosmosTokenMessageOptions[]; id: string;
error: string | undefined;
data: AuthorizationToken | undefined;
};
} }
| { | {
type: MessageTypes.GetAllResourceTokens; type: "allResourceTokens_v2";
message: {
id: string;
error: string | undefined;
data: FabricDatabaseConnectionInfo | undefined;
};
}
| {
type: "setToolbarStatus";
message: {
visible: boolean;
};
}; };
export type GetCosmosTokenMessageOptions = {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
resourceId: string;
};
export type CosmosDBTokenResponse = { export type CosmosDBTokenResponse = {
token: string; token: string;
date: string; date: string;
@@ -66,12 +86,9 @@ export type CosmosDBTokenResponse = {
export type CosmosDBConnectionInfoResponse = { export type CosmosDBConnectionInfoResponse = {
endpoint: string; endpoint: string;
databaseId: string; databaseId: string;
resourceTokens: unknown; resourceTokens: { [resourceId: string]: string };
}; };
export interface FabricDatabaseConnectionInfo { export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
endpoint: string;
databaseId: string;
resourceTokens: { [resourceId: string]: string };
resourceTokensTimestamp: number; resourceTokensTimestamp: number;
} }

View File

@@ -1,6 +1,12 @@
/** /**
* Messaging types used with Data Explorer <-> Portal communication, * Messaging types used with Data Explorer <-> Portal communication,
* Hosted <-> Explorer communication and Data Explorer -> Fabric communication. * Hosted <-> Explorer communication and Data Explorer -> Fabric communication.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* WARNING: !!!!!!! YOU CAN ONLY ADD NEW TYPES TO THE END OF THIS ENUM !!!!!!!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*
* Enum are integers, so inserting or deleting a type will break the communication.
*/ */
export enum MessageTypes { export enum MessageTypes {
TelemetryInfo, TelemetryInfo,
@@ -37,10 +43,10 @@ export enum MessageTypes {
DisplayNPSSurvey, DisplayNPSSurvey,
OpenVCoreMongoNetworkingBlade, OpenVCoreMongoNetworkingBlade,
OpenVCoreMongoConnectionStringsBlade, OpenVCoreMongoConnectionStringsBlade,
GetAuthorizationToken, // Data Explorer -> Fabric
// Data Explorer -> Fabric communication GetAllResourceTokens, // Data Explorer -> Fabric
GetAuthorizationToken, Ready, // Data Explorer -> Fabric
GetAllResourceTokens, OpenCESCVAFeedbackBlade,
} }
export interface AuthorizationToken { export interface AuthorizationToken {

View File

@@ -386,9 +386,9 @@ export interface DataExplorerInputsFrame {
dnsSuffix?: string; dnsSuffix?: string;
serverId?: string; serverId?: string;
extensionEndpoint?: string; extensionEndpoint?: string;
mongoProxyEndpoint?: string;
subscriptionType?: SubscriptionType; subscriptionType?: SubscriptionType;
quotaId?: string; quotaId?: string;
addCollectionDefaultFlight?: string;
isTryCosmosDBSubscription?: boolean; isTryCosmosDBSubscription?: boolean;
loadDatabaseAccountTimestamp?: number; loadDatabaseAccountTimestamp?: number;
sharedThroughputMinimum?: number; sharedThroughputMinimum?: number;

View File

@@ -5,9 +5,9 @@ import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil"; import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
@@ -265,63 +265,37 @@ export default class Explorer {
// TODO: return result // TODO: return result
} }
private getRandomInt(max: number) {
return Math.floor(Math.random() * max);
}
public openNPSSurveyDialog(): void { public openNPSSurveyDialog(): void {
if (!Platform.Portal) { if (!Platform.Portal) {
return; return;
} }
const NINETY_DAYS_IN_MS = 7776000000;
const ONE_DAY_IN_MS = 86400000; const ONE_DAY_IN_MS = 86400000;
const THREE_DAYS_IN_MS = 259200000; const SEVEN_DAYS_IN_MS = 604800000;
const isAccountNewerThanNinetyDays = isAccountNewerThanThresholdInMs(
userContext.databaseAccount?.systemData?.createdAt || "",
NINETY_DAYS_IN_MS,
);
const lastSubmitted: string = localStorage.getItem("lastSubmitted");
if (lastSubmitted !== null) {
let lastSubmittedDate: number = parseInt(lastSubmitted);
if (isNaN(lastSubmittedDate)) {
lastSubmittedDate = 0;
}
const nowMs: number = Date.now();
const millisecsSinceLastSubmitted = nowMs - lastSubmittedDate;
if (millisecsSinceLastSubmitted < NINETY_DAYS_IN_MS) {
return;
}
}
// Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer. // Try Cosmos DB subscription - survey shown to 100% of users at day 1 in Data Explorer.
if (userContext.isTryCosmosDBSubscription) { if (userContext.isTryCosmosDBSubscription) {
if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) { if (isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", ONE_DAY_IN_MS)) {
this.sendNPSMessage(); Logger.logInfo(
`Sending message to Portal to check if NPS Survey can be displayed in Try Cosmos DB ${userContext.apiType}`,
"Explorer/openNPSSurveyDialog",
);
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
} }
} else { } else {
// An existing account is older than 3 days but less than 90 days old. For existing account show to 100% of users in Data Explorer. // Show survey when an existing account is older than 7 days
if ( if (
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) && !isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
isAccountNewerThanNinetyDays
) { ) {
this.sendNPSMessage(); Logger.logInfo(
} else { `Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
// An existing account is greater than 90 days. For existing account show to random 33% of users in Data Explorer. "Explorer/openNPSSurveyDialog",
if (this.getRandomInt(100) < 33) { );
this.sendNPSMessage(); sendMessage({ type: MessageTypes.DisplayNPSSurvey });
}
} }
} }
} }
private sendNPSMessage() {
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
localStorage.setItem("lastSubmitted", Date.now().toString());
}
public async refreshDatabaseForResourceToken(): Promise<void> { public async refreshDatabaseForResourceToken(): Promise<void> {
const databaseId = userContext.parsedResourceToken?.databaseId; const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId; const collectionId = userContext.parsedResourceToken?.collectionId;
@@ -384,9 +358,7 @@ export default class Explorer {
public onRefreshResourcesClick = (): void => { public onRefreshResourcesClick = (): void => {
if (configContext.platform === Platform.Fabric) { if (configContext.platform === Platform.Fabric) {
// Requesting the tokens will trigger a refresh of the databases scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
requestDatabaseResourceTokens();
return; return;
} }

View File

@@ -24,16 +24,21 @@ interface Props {
export interface CommandBarStore { export interface CommandBarStore {
contextButtons: CommandButtonComponentProps[]; contextButtons: CommandButtonComponentProps[];
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void; setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
isHidden: boolean;
setIsHidden: (isHidden: boolean) => void;
} }
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
contextButtons: [], contextButtons: [],
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })), setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
})); }));
export const CommandBar: React.FC<Props> = ({ container }: Props) => { export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const selectedNodeState = useSelectedNode(); const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons); const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
const backgroundColor = StyleConstants.BaseLight; const backgroundColor = StyleConstants.BaseLight;
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
@@ -42,7 +47,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
? CommandBarComponentButtonFactory.createPostgreButtons(container) ? CommandBarComponentButtonFactory.createPostgreButtons(container)
: CommandBarComponentButtonFactory.createVCoreMongoButtons(container); : CommandBarComponentButtonFactory.createVCoreMongoButtons(container);
return ( return (
<div className="commandBarContainer"> <div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar <FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands" ariaLabel="Use left and right arrow keys to navigate between commands"
items={CommandBarUtil.convertButton(buttons, backgroundColor)} items={CommandBarUtil.convertButton(buttons, backgroundColor)}
@@ -91,7 +96,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
? { ? {
root: { root: {
backgroundColor: "transparent", backgroundColor: "transparent",
padding: "0px 14px 0px 14px", padding: "2px 8px 0px 8px",
}, },
} }
: { : {
@@ -101,7 +106,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
}; };
return ( return (
<div className="commandBarContainer"> <div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
<FluentCommandBar <FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands" ariaLabel="Use left and right arrow keys to navigate between commands"
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)} items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}

View File

@@ -135,7 +135,7 @@ export function createStaticCommandBarButtons(
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }
if (isQuerySupported && selectedNodeState.findSelectedCollection()) { if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
const openQueryBtn = createOpenQueryButton(container); const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn); buttons.push(openQueryBtn);

View File

@@ -25,7 +25,10 @@ import { MemoryTracker } from "./MemoryTrackerComponent";
* @param btns * @param btns
*/ */
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => { export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight; const buttonHeightPx =
configContext.platform == Platform.Fabric
? StyleConstants.FabricCommandBarButtonHeight
: StyleConstants.CommandBarButtonHeight;
const hoverColor = const hoverColor =
configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight; configContext.platform == Platform.Fabric ? StyleConstants.FabricAccentLight : StyleConstants.AccentLight;
@@ -112,6 +115,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
splitButtonContainer: { splitButtonContainer: {
marginLeft: 5, marginLeft: 5,
marginRight: 5, marginRight: 5,
height: buttonHeightPx,
}, },
}, },
className: btn.className, className: btn.className,

View File

@@ -1,12 +1,11 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react"; import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { SubscriptionType } from "../../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../../Contracts/SubscriptionType";
import { useSidePanel } from "../../../hooks/useSidePanel";
import * as SharedConstants from "../../../Shared/Constants"; import * as SharedConstants from "../../../Shared/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
@@ -14,6 +13,7 @@ import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../../Utils/PricingUtils"; import { getUpsellMessage } from "../../../Utils/PricingUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
@@ -63,9 +63,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
}, },
subscriptionType: SubscriptionType[subscriptionType], subscriptionType: SubscriptionType[subscriptionType],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
}; };
@@ -75,7 +72,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
throughput, throughput,
flight: userContext.addCollectionFlight,
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
}; };

View File

@@ -59,7 +59,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: newKeySpaceThroughput || tableThroughput, throughput: newKeySpaceThroughput || tableThroughput,
flight: userContext.addCollectionFlight,
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
}; };

View File

@@ -11,7 +11,13 @@ import {
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import {
DefaultRUThreshold,
LocalStorageUtility,
StorageKey,
getRUThreshold,
ruThresholdEnabled as isRUThresholdEnabled,
} from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
@@ -35,6 +41,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
? Constants.Queries.UnlimitedPageOption ? Constants.Queries.UnlimitedPageOption
: Constants.Queries.CustomPageOption, : Constants.Queries.CustomPageOption,
); );
const [ruThresholdEnabled, setRUThresholdEnabled] = useState<boolean>(isRUThresholdEnabled());
const [ruThreshold, setRUThreshold] = useState<number>(getRUThreshold());
const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>( const [queryTimeoutEnabled, setQueryTimeoutEnabled] = useState<boolean>(
LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
); );
@@ -103,6 +111,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage, isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
); );
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts); LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval); LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval);
@@ -120,6 +129,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
); );
} }
if (ruThresholdEnabled) {
LocalStorageUtility.setEntryNumber(StorageKey.RUThreshold, ruThreshold);
}
if (queryTimeoutEnabled) { if (queryTimeoutEnabled) {
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout); LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
LocalStorageUtility.setEntryBoolean( LocalStorageUtility.setEntryBoolean(
@@ -195,6 +208,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setPageOption(option.key); setPageOption(option.key);
}; };
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
setRUThresholdEnabled(checked);
};
const handleOnRUThresholdSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
const ruThreshold = Number(newValue);
if (!isNaN(ruThreshold)) {
setRUThreshold(ruThreshold);
}
};
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => { const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
setQueryTimeoutEnabled(checked); setQueryTimeoutEnabled(checked);
}; };
@@ -234,7 +258,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => { const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
setCopilotSampleDBEnabled(checked); setCopilotSampleDBEnabled(checked);
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked); useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
setRefreshExplorer(!refreshExplorer); setRefreshExplorer(false);
}; };
const choiceButtonStyles = { const choiceButtonStyles = {
@@ -259,7 +283,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
], ],
}; };
const queryTimeoutToggleStyles: IToggleStyles = { const toggleStyles: IToggleStyles = {
label: { label: {
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
@@ -272,7 +296,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
text: {}, text: {},
}; };
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = { const spinButtonStyles: ISpinButtonStyles = {
label: { label: {
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
@@ -338,48 +362,83 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</div> </div>
)} )}
{userContext.apiType === "SQL" && ( {userContext.apiType === "SQL" && (
<div className="settingsSection"> <>
<div className="settingsSectionPart"> <div className="settingsSection">
<div> <div className="settingsSectionPart">
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel"> <div>
Query Timeout <legend id="ruThresholdLabel" className="settingsSectionLabel legendLabel">
</legend> RU Threshold
<InfoTooltip> </legend>
When a query reaches a specified time limit, a popup with an option to cancel the query will show <InfoTooltip>If a query exceeds a configured RU threshold, the query will be aborted.</InfoTooltip>
unless automatic cancellation has been enabled </div>
</InfoTooltip>
</div>
<div>
<Toggle
styles={queryTimeoutToggleStyles}
label="Enable query timeout"
onChange={handleOnQueryTimeoutToggleChange}
defaultChecked={queryTimeoutEnabled}
/>
</div>
{queryTimeoutEnabled && (
<div> <div>
<SpinButton
label="Query timeout (ms)"
labelPosition={Position.top}
defaultValue={(queryTimeout || 5000).toString()}
min={100}
step={1000}
onChange={handleOnQueryTimeoutSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={queryTimeoutSpinButtonStyles}
/>
<Toggle <Toggle
label="Automatically cancel query after timeout" styles={toggleStyles}
styles={queryTimeoutToggleStyles} label="Enable RU threshold"
onChange={handleOnAutomaticallyCancelQueryToggleChange} onChange={handleOnRUThresholdToggleChange}
defaultChecked={automaticallyCancelQueryAfterTimeout} defaultChecked={ruThresholdEnabled}
/> />
</div> </div>
)} {ruThresholdEnabled && (
<div>
<SpinButton
label="RU Threshold (RU)"
labelPosition={Position.top}
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
min={1}
step={1000}
onChange={handleOnRUThresholdSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
</div>
)}
</div>
</div> </div>
</div> <div className="settingsSection">
<div className="settingsSectionPart">
<div>
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
Query Timeout
</legend>
<InfoTooltip>
When a query reaches a specified time limit, a popup with an option to cancel the query will show
unless automatic cancellation has been enabled
</InfoTooltip>
</div>
<div>
<Toggle
styles={toggleStyles}
label="Enable query timeout"
onChange={handleOnQueryTimeoutToggleChange}
defaultChecked={queryTimeoutEnabled}
/>
</div>
{queryTimeoutEnabled && (
<div>
<SpinButton
label="Query timeout (ms)"
labelPosition={Position.top}
defaultValue={(queryTimeout || 5000).toString()}
min={100}
step={1000}
onChange={handleOnQueryTimeoutSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
<Toggle
label="Automatically cancel query after timeout"
styles={toggleStyles}
onChange={handleOnAutomaticallyCancelQueryToggleChange}
defaultChecked={automaticallyCancelQueryAfterTimeout}
/>
</div>
)}
</div>
</div>
</>
)} )}
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
@@ -404,7 +463,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
styles={queryTimeoutSpinButtonStyles} styles={spinButtonStyles}
/> />
<div> <div>
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel"> <legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
@@ -426,7 +485,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
styles={queryTimeoutSpinButtonStyles} styles={spinButtonStyles}
/> />
<div> <div>
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel"> <legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
@@ -448,7 +507,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)} onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)} onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)} onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
styles={queryTimeoutSpinButtonStyles} styles={spinButtonStyles}
/> />
</div> </div>
</div> </div>

View File

@@ -97,6 +97,74 @@ exports[`Settings Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div>
<legend
className="settingsSectionLabel legendLabel"
id="ruThresholdLabel"
>
RU Threshold
</legend>
<InfoTooltip>
If a query exceeds a configured RU threshold, the query will be aborted.
</InfoTooltip>
</div>
<div>
<StyledToggleBase
defaultChecked={true}
label="Enable RU threshold"
onChange={[Function]}
styles={
Object {
"container": Object {},
"label": Object {
"display": "block",
"fontSize": 12,
"fontWeight": 400,
},
"pill": Object {},
"root": Object {},
"text": Object {},
"thumb": Object {},
}
}
/>
</div>
<div>
<StyledSpinButton
decrementButtonAriaLabel="Decrease value by 1000"
defaultValue="5000"
incrementButtonAriaLabel="Increase value by 1000"
label="RU Threshold (RU)"
labelPosition={0}
min={1}
onChange={[Function]}
step={1000}
styles={
Object {
"arrowButtonsContainer": Object {},
"icon": Object {},
"input": Object {},
"label": Object {
"fontSize": 12,
"fontWeight": 400,
},
"labelWrapper": Object {},
"root": Object {
"paddingBottom": 10,
},
"spinButtonWrapper": Object {},
}
}
/>
</div>
</div>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >

View File

@@ -1,5 +1,6 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
@@ -97,9 +98,19 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
/* Add new entity attribute */ /* Add new entity attribute */
const onSubmit = async (): Promise<void> => { const onSubmit = async (): Promise<void> => {
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
const { property, type } = entities[i]; const { property, type, value } = entities[i];
if (property === "" || property === undefined) { if ((property === "PartitionKey" && value === "") || (property === "RowKey" && value === "")) {
setFormError(`Property name cannot be empty. Please enter a property name`); logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
return;
}
if (
(property === "PartitionKey" && containsAnyWhiteSpace(value) === true) ||
(property === "RowKey" && containsAnyWhiteSpace(value) === true)
) {
logConsoleError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
setFormError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
return; return;
} }
@@ -107,6 +118,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`); setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return; return;
} }
setFormError("");
} }
setIsExecuting(true); setIsExecuting(true);
@@ -127,6 +140,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
} }
}; };
const containsAnyWhiteSpace = (entityValue: string) => {
if (/\s/.test(entityValue)) {
return true;
}
return false;
};
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => { const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
let newHeaders: string[] = []; let newHeaders: string[] = [];
const keys = Object.keys(newEntity); const keys = Object.keys(newEntity);
@@ -182,9 +202,14 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
const entityChange = (value: string | Date, indexOfInput: number, key: string): void => { const entityChange = (value: string | Date, indexOfInput: number, key: string): void => {
const cloneEntities: EntityRowType[] = [...entities]; const cloneEntities: EntityRowType[] = [...entities];
if (key === "property") { if (key === "property") {
cloneEntities[indexOfInput].property = value.toString(); cloneEntities[indexOfInput].property = value.toString().trim();
} else if (key === "time") { } else if (key === "time") {
cloneEntities[indexOfInput].entityTimeValue = value.toString(); cloneEntities[indexOfInput].entityTimeValue = value.toString();
} else if (
cloneEntities[indexOfInput].property === "PartitionKey" ||
cloneEntities[indexOfInput].property === "RowKey"
) {
cloneEntities[indexOfInput].value = value.toString().trim();
} else { } else {
cloneEntities[indexOfInput].value = value.toString(); cloneEntities[indexOfInput].value = value.toString();
} }

View File

@@ -1,5 +1,6 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react"; import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
@@ -190,7 +191,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
const onSubmit = async (): Promise<void> => { const onSubmit = async (): Promise<void> => {
for (let i = 0; i < entities.length; i++) { for (let i = 0; i < entities.length; i++) {
const { property, type } = entities[i]; const { property, type, value } = entities[i];
if (property === "" || property === undefined) { if (property === "" || property === undefined) {
setFormError(`Property name cannot be empty. Please enter a property name`); setFormError(`Property name cannot be empty. Please enter a property name`);
return; return;
@@ -200,6 +201,17 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`); setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return; return;
} }
if (
(property === "PartitionKey" && value === "") ||
(property === "PartitionKey" && value === undefined) ||
(property === "RowKey" && value === "") ||
(property === "RowKey" && value === undefined)
) {
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
return;
}
} }
setIsExecuting(true); setIsExecuting(true);
@@ -359,7 +371,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
selectedKey={entity.type} selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp} entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder} entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value?.toString()} entityValue={entity.value.toString()}
isEntityTypeDate={entity.isEntityTypeDate} isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue} entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable} isEntityValueDisable={entity.isEntityValueDisable}

View File

@@ -50,7 +50,9 @@ export const WelcomeModal = ({ visible }: { visible: boolean }): JSX.Element =>
</Stack> </Stack>
<Stack horizontalAlign="center"> <Stack horizontalAlign="center">
<Stack.Item align="center" style={{ textAlign: "center" }}> <Stack.Item align="center" style={{ textAlign: "center" }}>
<Text className="title bold">Welcome to Microsoft Copilot for Azure in Cosmos DB</Text> <Text className="title bold" as={"h1"}>
Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)
</Text>
</Stack.Item> </Stack.Item>
<Stack.Item align="center" className="text"> <Stack.Item align="center" className="text">
<Stack horizontal> <Stack horizontal>

View File

@@ -67,9 +67,10 @@ exports[`Query Copilot Welcome Modal snapshot test should render when isOpen is
} }
> >
<Text <Text
as="h1"
className="title bold" className="title bold"
> >
Welcome to Microsoft Copilot for Azure in Cosmos DB Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)
</Text> </Text>
</StackItem> </StackItem>
<StackItem <StackItem

View File

@@ -30,6 +30,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
queryResults: undefined, queryResults: undefined,
errorMessage: "", errorMessage: "",
isSamplePromptsOpen: false, isSamplePromptsOpen: false,
showPromptTeachingBubble: true,
showDeletePopup: false, showDeletePopup: false,
showFeedbackBar: false, showFeedbackBar: false,
showCopyPopup: false, showCopyPopup: false,
@@ -65,6 +66,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }), setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
setErrorMessage: (errorMessage: string) => set({ errorMessage }), setErrorMessage: (errorMessage: string) => set({ errorMessage }),
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }), setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => set({ showPromptTeachingBubble }),
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }), setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }), setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }), setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
@@ -103,6 +105,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
queryResults: undefined, queryResults: undefined,
errorMessage: "", errorMessage: "",
isSamplePromptsOpen: false, isSamplePromptsOpen: false,
showPromptTeachingBubble: true,
showDeletePopup: false, showDeletePopup: false,
showFeedbackBar: false, showFeedbackBar: false,
showCopyPopup: false, showCopyPopup: false,

View File

@@ -18,7 +18,6 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { HttpStatusCodes } from "Common/Constants"; import { HttpStatusCodes } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
@@ -71,7 +70,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
databaseId, databaseId,
containerId, containerId,
}: QueryCopilotPromptProps): JSX.Element => { }: QueryCopilotPromptProps): JSX.Element => {
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false);
const inputEdited = useRef(false); const inputEdited = useRef(false);
const { const {
openFeedbackModal, openFeedbackModal,
@@ -94,6 +93,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setIsSamplePromptsOpen, setIsSamplePromptsOpen,
showSamplePrompts, showSamplePrompts,
setShowSamplePrompts, setShowSamplePrompts,
showPromptTeachingBubble,
setShowPromptTeachingBubble,
showDeletePopup, showDeletePopup,
setShowDeletePopup, setShowDeletePopup,
showFeedbackBar, showFeedbackBar,
@@ -272,16 +273,23 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}; };
const showTeachingBubble = (): void => { const showTeachingBubble = (): void => {
if (!inputEdited.current) { if (showPromptTeachingBubble && !inputEdited.current) {
setTimeout(() => { setTimeout(() => {
if (!inputEdited.current && !isWelcomModalVisible()) { if (!inputEdited.current && !isWelcomModalVisible()) {
toggleCopilotTeachingBubbleVisible(); setCopilotTeachingBubbleVisible(true);
inputEdited.current = true; inputEdited.current = true;
} }
}, 30000); }, 30000);
} else {
toggleCopilotTeachingBubbleVisible(false);
} }
}; };
const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => {
setCopilotTeachingBubbleVisible(visible);
setShowPromptTeachingBubble(visible);
};
const isWelcomModalVisible = (): boolean => { const isWelcomModalVisible = (): boolean => {
return localStorage.getItem("hideWelcomeModal") !== "true"; return localStorage.getItem("hideWelcomeModal") !== "true";
}; };
@@ -340,6 +348,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, },
}} }}
ariaLabel="Close" ariaLabel="Close"
title="Close copilot"
/> />
</Stack> </Stack>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
@@ -364,13 +373,13 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
placeholder="Ask a question in natural language and well generate the query for you." placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label" aria-labelledby="copilot-textfield-label"
/> />
{copilotTeachingBubbleVisible && ( {showPromptTeachingBubble && copilotTeachingBubbleVisible && (
<TeachingBubble <TeachingBubble
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }} calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
target="#naturalLanguageInput" target="#naturalLanguageInput"
hasCloseButton={true} hasCloseButton={true}
closeButtonAriaLabel="Close" closeButtonAriaLabel="Close"
onDismiss={toggleCopilotTeachingBubbleVisible} onDismiss={() => toggleCopilotTeachingBubbleVisible(false)}
hasSmallHeadline={true} hasSmallHeadline={true}
headline="Write a prompt" headline="Write a prompt"
> >
@@ -378,7 +387,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Link <Link
onClick={() => { onClick={() => {
setShowSamplePrompts(true); setShowSamplePrompts(true);
toggleCopilotTeachingBubbleVisible(); toggleCopilotTeachingBubbleVisible(false);
}} }}
style={{ color: "white", fontWeight: 600 }} style={{ color: "white", fontWeight: 600 }}
> >
@@ -534,6 +543,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text> <Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
{showCallout && !hideFeedbackModalForLikedQueries && ( {showCallout && !hideFeedbackModalForLikedQueries && (
<Callout <Callout
role="status"
style={{ padding: 8 }} style={{ padding: 8 }}
target="#likeBtn" target="#likeBtn"
onDismiss={() => { onDismiss={() => {
@@ -569,11 +579,18 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<IconButton <IconButton
id="likeBtn" id="likeBtn"
style={{ marginLeft: 20 }} style={{ marginLeft: 20 }}
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
aria-label="Like" aria-label="Like"
role="toggle"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => { onClick={() => {
setShowCallout(!likeQuery); setShowCallout(!likeQuery);
setLikeQuery(!likeQuery); setLikeQuery(!likeQuery);
if (likeQuery === true) {
document.getElementById("likeStatus").innerHTML = "Unpressed";
}
if (likeQuery === false) {
document.getElementById("likeStatus").innerHTML = "Liked";
}
if (dislikeQuery) { if (dislikeQuery) {
setDislikeQuery(!dislikeQuery); setDislikeQuery(!dislikeQuery);
} }
@@ -581,17 +598,24 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
/> />
<IconButton <IconButton
style={{ margin: "0 10px" }} style={{ margin: "0 10px" }}
role="toggle"
aria-label="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }} iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => { onClick={() => {
let toggleStatusValue = "Unpressed";
if (!dislikeQuery) { if (!dislikeQuery) {
openFeedbackModal(generatedQuery, false, userPrompt); openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false); setLikeQuery(false);
toggleStatusValue = "Disliked";
} }
setDislikeQuery(!dislikeQuery); setDislikeQuery(!dislikeQuery);
setShowCallout(false); setShowCallout(false);
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
}} }}
aria-label="Dislike"
/> />
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
<Separator vertical style={{ color: "#EDEBE9" }} /> <Separator vertical style={{ color: "#EDEBE9" }} />
<CommandBarButton <CommandBarButton
onClick={copyGeneratedCode} onClick={copyGeneratedCode}

View File

@@ -10,7 +10,7 @@ import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotCl
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults"; import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs"; import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -37,7 +37,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const executeQueryBtn = { const executeQueryBtn = {
iconSrc: ExecuteQueryIcon, iconSrc: ExecuteQueryIcon,
iconAlt: executeQueryBtnLabel, iconAlt: executeQueryBtnLabel,
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot), onCommandClick: () => OnExecuteQueryClick(useQueryCopilot as Partial<QueryCopilotState>),
commandButtonLabel: executeQueryBtnLabel, commandButtonLabel: executeQueryBtnLabel,
ariaLabel: executeQueryBtnLabel, ariaLabel: executeQueryBtnLabel,
hasPopup: false, hasPopup: false,

View File

@@ -1,6 +1,6 @@
import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import React from "react"; import React from "react";
export const QueryCopilotResults: React.FC = (): JSX.Element => { export const QueryCopilotResults: React.FC = (): JSX.Element => {
@@ -12,7 +12,11 @@ export const QueryCopilotResults: React.FC = (): JSX.Element => {
queryResults={useQueryCopilot.getState().queryResults} queryResults={useQueryCopilot.getState().queryResults}
isExecuting={useQueryCopilot.getState().isExecuting} isExecuting={useQueryCopilot.getState().isExecuting}
executeQueryDocumentsPage={(firstItemIndex: number) => executeQueryDocumentsPage={(firstItemIndex: number) =>
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot) QueryDocumentsPerPage(
firstItemIndex,
useQueryCopilot.getState().queryIterator,
useQueryCopilot as Partial<QueryCopilotState>,
)
} }
/> />
); );

View File

@@ -24,7 +24,9 @@ export const QuickstartCarousel: React.FC<QuickstartCarouselProps> = ({
> >
<Stack> <Stack>
<Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}> <Stack horizontal horizontalAlign="space-between" style={{ padding: 16 }}>
<Text variant="xLarge">{getHeaderText(page)}</Text> <Text role="heading" aria-level={1} variant="xLarge">
{getHeaderText(page)}
</Text>
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => setPage(4)} ariaLabel="Close" /> <IconButton iconProps={{ iconName: "Cancel" }} onClick={() => setPage(4)} ariaLabel="Close" />
</Stack> </Stack>
{getContent(page)} {getContent(page)}

View File

@@ -148,7 +148,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
/> />
</Stack> </Stack>
<Stack horizontal tokens={{ childrenGap: 16 }}> <Stack horizontal tokens={{ childrenGap: 16 }}>
{useQueryCopilot.getState().copilotEnabled && useQueryCopilot.getState().copilotSampleDBEnabled && ( {useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton <SplashScreenButton
imgSrc={CopilotIcon} imgSrc={CopilotIcon}
title={"Query faster with Copilot"} title={"Query faster with Copilot"}

View File

@@ -881,6 +881,11 @@ export default class DocumentsTab extends TabsBase {
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
if (!userContext.hasWriteAccess) {
// All the following buttons require write access
return [];
}
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document"; const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
if (this.newDocumentButton.visible()) { if (this.newDocumentButton.visible()) {

View File

@@ -3,13 +3,13 @@ import {
DetailsListLayoutMode, DetailsListLayoutMode,
IColumn, IColumn,
Icon, Icon,
IconButton,
Link, Link,
Pivot, Pivot,
PivotItem, PivotItem,
SelectionMode, SelectionMode,
Stack, Stack,
Text, Text,
IconButton,
TooltipHost, TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import { HttpHeaders, NormalizedEventKey } from "Common/Constants"; import { HttpHeaders, NormalizedEventKey } from "Common/Constants";
@@ -18,15 +18,15 @@ import { QueryMetrics } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent"; import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import copy from "clipboard-copy";
import { useNotificationConsole } from "hooks/useNotificationConsole"; import { useNotificationConsole } from "hooks/useNotificationConsole";
import React from "react"; import React from "react";
import CopilotCopy from "../../../../images/CopilotCopy.svg";
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg"; import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg"; import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
import RunQuery from "../../../../images/RunQuery.png"; import RunQuery from "../../../../images/RunQuery.png";
import InfoColor from "../../../../images/info_color.svg"; import InfoColor from "../../../../images/info_color.svg";
import { QueryResults } from "../../../Contracts/ViewModels"; import { QueryResults } from "../../../Contracts/ViewModels";
import copy from "clipboard-copy";
import CopilotCopy from "../../../../images/CopilotCopy.svg";
interface QueryResultProps { interface QueryResultProps {
isMongoDB: boolean; isMongoDB: boolean;
@@ -62,9 +62,12 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
const columns: IColumn[] = [ const columns: IColumn[] = [
{ {
key: "column1", key: "column1",
name: "", name: "Description",
iconName: "Info",
isIconOnly: true,
minWidth: 10, minWidth: 10,
maxWidth: 12, maxWidth: 12,
iconClassName: "iconheadercell",
data: String, data: String,
fieldName: "", fieldName: "",
onRender: (item: IDocument) => { onRender: (item: IDocument) => {

View File

@@ -91,9 +91,6 @@
div[role="tabpanel"] { div[role="tabpanel"] {
height: 100%; height: 100%;
div:nth-child(1) {
height: 100%;
}
} }
.result-metadata { .result-metadata {
@@ -283,3 +280,6 @@
} }
} }
} }
.iconheadercell {
font-size: 12px;
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */ /* eslint-disable no-console */
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
@@ -10,7 +11,7 @@ import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopil
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { TabsState, useTabs } from "hooks/useTabs"; import { TabsState, useTabs } from "hooks/useTabs";
@@ -303,8 +304,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
isExecutionError: false, isExecutionError: false,
}); });
let queryOperationOptions: QueryOperationOptions;
if (userContext.apiType === "SQL" && ruThresholdEnabled()) {
const ruThreshold: number = getRUThreshold();
queryOperationOptions = {
ruCapPerOperation: ruThreshold,
} as QueryOperationOptions;
}
const queryDocuments = async (firstItemIndex: number) => const queryDocuments = async (firstItemIndex: number) =>
await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex); await queryDocumentsPage(
this.props.collection && this.props.collection.id(),
this._iterator,
firstItemIndex,
queryOperationOptions,
);
this.props.tabsBaseInstance.isExecuting(true); this.props.tabsBaseInstance.isExecuting(true);
this.setState({ this.setState({
isExecuting: true, isExecuting: true,
@@ -390,7 +403,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}); });
} }
if (this.saveQueryButton.visible) { if (this.saveQueryButton.visible && configContext.platform !== Platform.Fabric) {
const label = "Save Query"; const label = "Save Query";
buttons.push({ buttons.push({
iconSrc: SaveQueryIcon, iconSrc: SaveQueryIcon,

View File

@@ -10,6 +10,7 @@ import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
@@ -29,7 +30,9 @@ interface TabsProps {
export const Tabs = ({ explorer }: TabsProps): JSX.Element => { export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs(); const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(),
);
return ( return (
<div className="tabsManagerContainer"> <div className="tabsManagerContainer">
{networkSettingsWarning && ( {networkSettingsWarning && (
@@ -54,6 +57,23 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
{networkSettingsWarning} {networkSettingsWarning}
</MessageBar> </MessageBar>
)} )}
{showRUThresholdMessageBar && (
<MessageBar
messageBarType={MessageBarType.info}
onDismiss={() => {
setShowRUThresholdMessageBar(false);
}}
styles={{
innerText: {
fontWeight: "bold",
},
}}
>
{
"To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove the limit, go to the Settings cog on the right and find 'RU Threshold'."
}
</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">

View File

@@ -4,9 +4,9 @@ import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createTrigger } from "../../Common/dataAccess/createTrigger"; import { createTrigger } from "../../Common/dataAccess/createTrigger";
import { updateTrigger } from "../../Common/dataAccess/updateTrigger"; import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -68,7 +68,9 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
return true; return true;
}, },
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => { findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
return get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase); return isSampleDatabase === undefined
? get().databases.find((db) => databaseId === db.id())
: get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
}, },
isLastNonEmptyDatabase: () => { isLastNonEmptyDatabase: () => {
const databases = get().databases; const databases = get().databases;

View File

@@ -1,6 +1,6 @@
import ko from "knockout"; import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import { GetGithubClientId } from "Utils/GitHubUtils"; import { GetGithubClientId } from "Utils/GitHubUtils";
import ko from "knockout";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";

View File

@@ -2,7 +2,7 @@ import { configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation"; import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { import {

View File

@@ -1,33 +1,45 @@
import { sendCachedDataMessage } from "Common/MessageHandler"; import { sendCachedDataMessage } from "Common/MessageHandler";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract"; import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
import { MessageTypes } from "Contracts/MessageTypes"; import { MessageTypes } from "Contracts/MessageTypes";
import Explorer from "Explorer/Explorer"; import { updateUserContext, userContext } from "UserContext";
import { updateUserContext } from "UserContext"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second
let timeoutId: NodeJS.Timeout; let timeoutId: NodeJS.Timeout;
// Prevents multiple parallel requests // Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
let isRequestPending = false; let lastRequestTimestamp: number = undefined;
export const requestDatabaseResourceTokens = (): void => { const requestDatabaseResourceTokens = async (): Promise<void> => {
if (isRequestPending) { if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
return; return;
} }
// TODO Make Fabric return the message id so we can handle this promise lastRequestTimestamp = Date.now();
isRequestPending = true; try {
sendCachedDataMessage<FabricDatabaseConnectionInfo>(MessageTypes.GetAllResourceTokens, []); const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
}; MessageTypes.GetAllResourceTokens,
[],
userContext.fabricContext.connectionId,
);
export const handleRequestDatabaseResourceTokensResponse = ( if (!userContext.databaseAccount.properties.documentEndpoint) {
explorer: Explorer, userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo, }
): void => {
isRequestPending = false; updateUserContext({
updateUserContext({ fabricDatabaseConnectionInfo }); fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo },
scheduleRefreshDatabaseResourceToken(); databaseAccount: { ...userContext.databaseAccount },
explorer.refreshAllDatabases(); hasWriteAccess: false, // TODO: receive from fabricDatabaseConnectionInfo
});
scheduleRefreshDatabaseResourceToken();
} catch (error) {
logConsoleError(error);
throw error;
} finally {
lastRequestTimestamp = undefined;
}
}; };
/** /**
@@ -35,19 +47,24 @@ export const handleRequestDatabaseResourceTokensResponse = (
* @param tokenTimestamp * @param tokenTimestamp
* @returns * @returns
*/ */
export const scheduleRefreshDatabaseResourceToken = (): void => { export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
if (timeoutId !== undefined) { return new Promise((resolve) => {
clearTimeout(timeoutId); if (timeoutId !== undefined) {
timeoutId = undefined; clearTimeout(timeoutId);
} timeoutId = undefined;
}
timeoutId = setTimeout(() => { timeoutId = setTimeout(
requestDatabaseResourceTokens(); () => {
}, TOKEN_VALIDITY_MS); requestDatabaseResourceTokens().then(resolve);
},
refreshNow ? 0 : TOKEN_VALIDITY_MS,
);
});
}; };
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => { export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) { if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
requestDatabaseResourceTokens(); scheduleRefreshDatabaseResourceToken(true);
} }
}; };

View File

@@ -172,7 +172,6 @@ export class CollectionCreation {
public static readonly DefaultCollectionRUs100K: number = 100000; public static readonly DefaultCollectionRUs100K: number = 100000;
public static readonly DefaultCollectionRUs1Million: number = 1000000; public static readonly DefaultCollectionRUs1Million: number = 1000000;
public static readonly DefaultAddCollectionDefaultFlight: string = "0";
public static readonly DefaultSubscriptionType: SubscriptionType = SubscriptionType.Free; public static readonly DefaultSubscriptionType: SubscriptionType = SubscriptionType.Free;
public static readonly TablesAPIDefaultDatabase: string = "TablesDB"; public static readonly TablesAPIDefaultDatabase: string = "TablesDB";

View File

@@ -1,9 +1,12 @@
import * as LocalStorageUtility from "./LocalStorageUtility"; import * as LocalStorageUtility from "./LocalStorageUtility";
import * as SessionStorageUtility from "./SessionStorageUtility"; import * as SessionStorageUtility from "./SessionStorageUtility";
import * as StringUtility from "./StringUtility";
export { LocalStorageUtility, SessionStorageUtility }; export { LocalStorageUtility, SessionStorageUtility };
export enum StorageKey { export enum StorageKey {
ActualItemPerPage, ActualItemPerPage,
RUThresholdEnabled,
RUThreshold,
QueryTimeoutEnabled, QueryTimeoutEnabled,
QueryTimeout, QueryTimeout,
RetryAttempts, RetryAttempts,
@@ -25,3 +28,27 @@ export enum StorageKey {
VisitedAccounts, VisitedAccounts,
PriorityLevel, PriorityLevel,
} }
export const hasRUThresholdBeenConfigured = (): boolean => {
const ruThresholdEnabledLocalStorageRaw: string | null = LocalStorageUtility.getEntryString(
StorageKey.RUThresholdEnabled,
);
return ruThresholdEnabledLocalStorageRaw === "true" || ruThresholdEnabledLocalStorageRaw === "false";
};
export const ruThresholdEnabled = (): boolean => {
const ruThresholdEnabledLocalStorageRaw: string | null = LocalStorageUtility.getEntryString(
StorageKey.RUThresholdEnabled,
);
return ruThresholdEnabledLocalStorageRaw === null || StringUtility.toBoolean(ruThresholdEnabledLocalStorageRaw);
};
export const getRUThreshold = (): number => {
const ruThresholdRaw = LocalStorageUtility.getEntryNumber(StorageKey.RUThreshold);
if (ruThresholdRaw !== 0) {
return ruThresholdRaw;
}
return DefaultRUThreshold;
};
export const DefaultRUThreshold = 5000;

View File

@@ -1,4 +1,4 @@
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract"; import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -47,8 +47,13 @@ export interface VCoreMongoConnectionParams {
connectionString: string; connectionString: string;
} }
interface FabricContext {
connectionId: string;
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
}
interface UserContext { interface UserContext {
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo; readonly fabricContext?: FabricContext;
readonly authType?: AuthType; readonly authType?: AuthType;
readonly masterKey?: string; readonly masterKey?: string;
readonly subscriptionId?: string; readonly subscriptionId?: string;
@@ -67,7 +72,6 @@ interface UserContext {
readonly isTryCosmosDBSubscription?: boolean; readonly isTryCosmosDBSubscription?: boolean;
readonly portalEnv?: PortalEnv; readonly portalEnv?: PortalEnv;
readonly features: Features; readonly features: Features;
readonly addCollectionFlight: string;
readonly hasWriteAccess: boolean; readonly hasWriteAccess: boolean;
readonly parsedResourceToken?: { readonly parsedResourceToken?: {
databaseId: string; databaseId: string;
@@ -94,7 +98,6 @@ const userContext: UserContext = {
isTryCosmosDBSubscription: false, isTryCosmosDBSubscription: false,
portalEnv: "prod", portalEnv: "prod",
features, features,
addCollectionFlight: CollectionCreation.DefaultAddCollectionDefaultFlight,
subscriptionType: CollectionCreation.DefaultSubscriptionType, subscriptionType: CollectionCreation.DefaultSubscriptionType,
collectionCreationDefaults: CollectionCreationDefaults, collectionCreationDefaults: CollectionCreationDefaults,
}; };

View File

@@ -67,7 +67,23 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
//usnat: ["7.28.202.68"], //usnat: ["7.28.202.68"],
}; };
export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238";
public static readonly MPAC: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
}
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [ export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.MPAC,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
];
export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> = [
"https://main.documentdb.ext.azure.com", "https://main.documentdb.ext.azure.com",
"https://main.documentdb.ext.azure.cn", "https://main.documentdb.ext.azure.cn",
"https://main.documentdb.ext.azure.us", "https://main.documentdb.ext.azure.us",

View File

@@ -1,7 +1,7 @@
import { resetConfigContext, updateConfigContext } from "ConfigContext"; import { resetConfigContext, updateConfigContext } from "ConfigContext";
import { DatabaseAccount, IpRule } from "Contracts/DataModels"; import { DatabaseAccount, IpRule } from "Contracts/DataModels";
import { updateUserContext } from "UserContext"; import { updateUserContext } from "UserContext";
import { PortalBackendIPs } from "Utils/EndpointValidation"; import { PortalBackendIPs } from "Utils/EndpointUtils";
import { getNetworkSettingsWarningMessage } from "./NetworkUtility"; import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
describe("NetworkUtility tests", () => { describe("NetworkUtility tests", () => {

View File

@@ -1,7 +1,7 @@
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules"; import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { PortalBackendIPs } from "Utils/EndpointValidation"; import { PortalBackendIPs } from "Utils/EndpointUtils";
export const getNetworkSettingsWarningMessage = async ( export const getNetworkSettingsWarningMessage = async (
setStateFunc: (warningMessage: string) => void, setStateFunc: (warningMessage: string) => void,

View File

@@ -1,11 +1,10 @@
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { FabricDatabaseConnectionInfo, FabricMessage } from "Contracts/FabricContract"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
import { FABRIC_RPC_VERSION, FabricMessageV2 } from "Contracts/FabricMessagesContract";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
handleRequestDatabaseResourceTokensResponse,
scheduleRefreshDatabaseResourceToken,
} from "Platform/Fabric/FabricUtil";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -34,7 +33,6 @@ import {
getDatabaseAccountPropertiesFromMetadata, getDatabaseAccountPropertiesFromMetadata,
} from "../Platform/Hosted/HostedUtils"; } from "../Platform/Hosted/HostedUtils";
import { extractFeatures } from "../Platform/Hosted/extractFeatures"; import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { CollectionCreation } from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"; import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader, getMsalInstance } from "../Utils/AuthorizationUtils";
@@ -88,6 +86,9 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
} }
async function configureFabric(): Promise<Explorer> { async function configureFabric(): Promise<Explorer> {
// These are the versions of Fabric that Data Explorer supports.
const SUPPORTED_FABRIC_VERSIONS = [FABRIC_RPC_VERSION];
let explorer: Explorer; let explorer: Explorer;
return new Promise<Explorer>((resolve) => { return new Promise<Explorer>((resolve) => {
window.addEventListener( window.addEventListener(
@@ -101,38 +102,37 @@ async function configureFabric(): Promise<Explorer> {
return; return;
} }
const data: FabricMessage = event.data?.data; const data: FabricMessageV2 = event.data?.data;
if (!data) { if (!data) {
return; return;
} }
switch (data.type) { switch (data.type) {
case "initialize": { case "initialize": {
const fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo = { const fabricVersion = data.version;
endpoint: data.message.endpoint, if (!SUPPORTED_FABRIC_VERSIONS.includes(fabricVersion)) {
databaseId: data.message.databaseId, // TODO Surface error to user
resourceTokens: data.message.resourceTokens as { [resourceId: string]: string }, console.error(`Unsupported Fabric version: ${fabricVersion}`);
resourceTokensTimestamp: data.message.resourceTokensTimestamp, return;
}; }
explorer = await createExplorerFabric(fabricDatabaseConnectionInfo);
resolve(explorer);
explorer.refreshAllDatabases().then(() => { explorer = createExplorerFabric(data.message);
openFirstContainer(explorer, fabricDatabaseConnectionInfo.databaseId); await scheduleRefreshDatabaseResourceToken(true);
}); resolve(explorer);
scheduleRefreshDatabaseResourceToken(); await explorer.refreshAllDatabases();
openFirstContainer(explorer, userContext.fabricContext.databaseConnectionInfo.databaseId);
break; break;
} }
case "newContainer": case "newContainer":
explorer.onNewCollectionClicked(); explorer.onNewCollectionClicked();
break; break;
case "authorizationToken": { case "authorizationToken":
case "allResourceTokens_v2": {
handleCachedDataMessage(data); handleCachedDataMessage(data);
break; break;
} }
case "allResourceTokens": { case "setToolbarStatus": {
// TODO call handleCachedDataMessage when Fabric echoes message id back useCommandBar.getState().setIsHidden(data.message.visible === false);
handleRequestDatabaseResourceTokensResponse(explorer, data.message as FabricDatabaseConnectionInfo);
break; break;
} }
default: default:
@@ -143,7 +143,11 @@ async function configureFabric(): Promise<Explorer> {
false, false,
); );
sendReadyMessage(); sendMessage({
type: MessageTypes.Ready,
id: "ready",
params: [DATA_EXPLORER_RPC_VERSION],
});
}); });
} }
@@ -319,9 +323,12 @@ function configureHostedWithResourceToken(config: ResourceToken): Explorer {
return explorer; return explorer;
} }
function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo): Explorer { function createExplorerFabric(params: { connectionId: string }): Explorer {
updateUserContext({ updateUserContext({
fabricDatabaseConnectionInfo, fabricContext: {
connectionId: params.connectionId,
databaseConnectionInfo: undefined,
},
authType: AuthType.ConnectionString, authType: AuthType.ConnectionString,
databaseAccount: { databaseAccount: {
id: "", id: "",
@@ -330,7 +337,7 @@ function createExplorerFabric(fabricDatabaseConnectionInfo: FabricDatabaseConnec
name: "Mounted", name: "Mounted",
kind: AccountKind.Default, kind: AccountKind.Default,
properties: { properties: {
documentEndpoint: fabricDatabaseConnectionInfo.endpoint, documentEndpoint: undefined,
}, },
}, },
}); });
@@ -471,6 +478,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
updateConfigContext({ updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT, BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT), ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
MONGO_PROXY_ENDPOINT: inputs.mongoProxyEndpoint,
}); });
updateUserContext({ updateUserContext({
@@ -483,7 +491,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
quotaId: inputs.quotaId, quotaId: inputs.quotaId,
portalEnv: inputs.serverId as PortalEnv, portalEnv: inputs.serverId as PortalEnv,
hasWriteAccess: inputs.hasWriteAccess ?? true, hasWriteAccess: inputs.hasWriteAccess ?? true,
addCollectionFlight: inputs.addCollectionDefaultFlight || CollectionCreation.DefaultAddCollectionDefaultFlight,
collectionCreationDefaults: inputs.defaultCollectionThroughput, collectionCreationDefaults: inputs.defaultCollectionThroughput,
isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription, isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription,
}); });

View File

@@ -29,6 +29,7 @@ export interface QueryCopilotState {
queryResults: QueryResults | undefined; queryResults: QueryResults | undefined;
errorMessage: string; errorMessage: string;
isSamplePromptsOpen: boolean; isSamplePromptsOpen: boolean;
showPromptTeachingBubble: boolean;
showDeletePopup: boolean; showDeletePopup: boolean;
showFeedbackBar: boolean; showFeedbackBar: boolean;
showCopyPopup: boolean; showCopyPopup: boolean;
@@ -71,6 +72,7 @@ export interface QueryCopilotState {
setQueryResults: (queryResults: QueryResults | undefined) => void; setQueryResults: (queryResults: QueryResults | undefined) => void;
setErrorMessage: (errorMessage: string) => void; setErrorMessage: (errorMessage: string) => void;
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void; setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => void;
setShowPromptTeachingBubble: (showPromptTeachingBubble: boolean) => void;
setShowDeletePopup: (showDeletePopup: boolean) => void; setShowDeletePopup: (showDeletePopup: boolean) => void;
setShowFeedbackBar: (showFeedbackBar: boolean) => void; setShowFeedbackBar: (showFeedbackBar: boolean) => void;
setshowCopyPopup: (showCopyPopup: boolean) => void; setshowCopyPopup: (showCopyPopup: boolean) => void;
@@ -93,7 +95,7 @@ export interface QueryCopilotState {
resetQueryCopilotStates: () => void; resetQueryCopilotStates: () => void;
} }
type QueryCopilotStore = UseStore<QueryCopilotState>; type QueryCopilotStore = UseStore<Partial<QueryCopilotState>>;
export const useQueryCopilot: QueryCopilotStore = create((set) => ({ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
copilotEnabled: false, copilotEnabled: false,

View File

@@ -54,7 +54,6 @@ const initTestExplorer = async (): Promise<void> => {
extensionEndpoint: "/proxy", extensionEndpoint: "/proxy",
subscriptionType: 3, subscriptionType: 3,
quotaId: "Internal_2014-09-01", quotaId: "Internal_2014-09-01",
addCollectionDefaultFlight: "2",
isTryCosmosDBSubscription: false, isTryCosmosDBSubscription: false,
masterKey: keys.primaryMasterKey, masterKey: keys.primaryMasterKey,
loadDatabaseAccountTimestamp: 1604663109836, loadDatabaseAccountTimestamp: 1604663109836,

View File

@@ -112,6 +112,7 @@
"./src/Utils/BlobUtils.ts", "./src/Utils/BlobUtils.ts",
"./src/Utils/CapabilityUtils.ts", "./src/Utils/CapabilityUtils.ts",
"./src/Utils/CloudUtils.ts", "./src/Utils/CloudUtils.ts",
"./src/Utils/EndpointUtils.ts",
"./src/Utils/GitHubUtils.test.ts", "./src/Utils/GitHubUtils.test.ts",
"./src/Utils/GitHubUtils.ts", "./src/Utils/GitHubUtils.ts",
"./src/Utils/MessageValidation.test.ts", "./src/Utils/MessageValidation.test.ts",