mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 13:51:49 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ada04da73 | ||
|
|
e43b4eee5c | ||
|
|
8b0b3b07d6 | ||
|
|
f403b086ad | ||
|
|
f8cfc6c21c | ||
|
|
35ca7944ae | ||
|
|
6d98b4a500 | ||
|
|
2d06eef9cc | ||
|
|
31f7178669 | ||
|
|
c0b54f6e84 | ||
|
|
cd3eb5b5b3 | ||
|
|
dbb0324a64 | ||
|
|
e207f3702b | ||
|
|
f496220ed6 | ||
|
|
eb790d09b5 | ||
|
|
323305e485 | ||
|
|
70635e426f | ||
|
|
5a5bf34d4d | ||
|
|
0975591945 |
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
18
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/Contracts/DataExplorerMessagesContract.ts
Normal file
42
src/Contracts/DataExplorerMessagesContract.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 we’ll generate the query for you."
|
placeholder="Ask a question in natural language and we’ll 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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user