Compare commits

...

31 Commits

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

* merge main into current

* allow mongo proxy endpoints to be constants

* allow mongo proxy endpoints to be constants

* fix test

* check for allowed mongo proxy endpoint

* check for allowed mongo proxy endpoint

---------

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

* limit RU threshold only to NoSQL

---------

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

* Run npm format

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

* use new ru threshold package

* fix typo

* fix merge issue

* fix package-lock.json

* fix test

* fixed settings pane test

* fixed merge issue

* sync with main

* fixed settings pane check

* fix checks

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* remove learn more

* change default RU threshold to 5000

* change default RU threshold to 5000

* change default RU threshold to 5000

---------

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

* Reverted value is not required unless Row or PrimaryKey

* Prettier Run

* Add full whitespace check on Keys

* Prettier formatting

* Fixed type

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

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

* use new ru threshold package

* fix typo

* fix merge issue

* fix package-lock.json

* fix test

* fixed settings pane test

* fixed merge issue

* sync with main

* fixed settings pane check

* fix checks

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* fixed aria-label error

* remove learn more

---------

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

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

* add close copilot button title

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

* Remove lint error

* Address comments

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

* Improve rpc and update initalization flow

* Fix format

* Rev up message names for new version

* Refactor RPC with Fabric

* Build fix

* Fix build

* Fix format

* Update Message types

* Fix format

* Fix comments

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

* Add Fabric specific Toolbar design

* Add Fabric message to show/hide the Toolbar

* Fix CommandBarUtil formatting

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

---------

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

* Fix format

---------

Co-authored-by: Vsevolod Kukol <sevoku@microsoft.com>
2024-01-18 17:13:04 +01:00
sunghyunkang1111
5d13463bdb Copilot settings (#1719)
* gating copilot with AFEC

* Add enable sample DB in settings

* Add enable sample DB in settings

* Add enable sample DB in settings

* PR comments
2024-01-09 13:32:20 -06:00
MokireddySampath
c4cceceafc Status attribute added for the message to be readout by screenreader (#1709) 2024-01-04 20:43:44 +05:30
MokireddySampath
532a453f5a Aria label has been updated to the button(Enable copilot/Disable copilot) (#1706) 2024-01-04 19:38:05 +05:30
MokireddySampath
9355a3ae04 Altt text and role has been added to the 'copilot icon' (#1705) 2024-01-04 19:37:50 +05:30
MokireddySampath
14456c2102 label has been added to the textfield of copilot promptbar (#1704) 2024-01-04 19:37:35 +05:30
MokireddySampath
0c9264e8b3 Bug 2819239:Screen reader does not announce the loading information which appears on invoking the 'Send' button. (#1703)
* Alert has been added and content also added accordign to the loader state

* Snapshot of tests have been updated
2024-01-04 19:37:16 +05:30
MokireddySampath
0dd1032357 Bug 2817823: [Programmatic access - Cosmos DB Query Copilot - Query]: Accessible name is not defined for the 'Like', 'Dislike' and 'Send' buttons present on the page. (#1700)
* text color of link has been changed to get the contrast ratio of atleaast 4.5:1

* screen reader names have been added to the buttons

* Update QueryCopilotPromptbar.tsx
2024-01-04 19:35:12 +05:30
MokireddySampath
5011d12f16 text color of link has been changed to get the contrast ratio of atleaast 4.5:1 (#1699) 2024-01-04 19:34:58 +05:30
MokireddySampath
a7e5ff2a9f status role has been added for the screen reader to announce the status message displayed (#1697) 2024-01-04 19:34:26 +05:30
MokireddySampath
ad1391f623 visual and arialabel were different which has been changed as required and tests have been updated (#1685) 2024-01-04 19:34:14 +05:30
MokireddySampath
a2a5407b15 Label has been added to the text field on selecting autoscale in throughputc (#1676) 2024-01-04 19:33:55 +05:30
Laurent Nguyen
e9181f19d7 Fix datatables issue and indicator not loading for Table API > Entities. Upgrade jquery. Fix right panel resize issue. (#1713)
* Fix datatables issue and indicator not loading for Table API > Entities

* Fix jquery and datatables compile issues. Add patch for datatables.net-colreorder error in types

* Fix side panel size. Fix bug resizing side panel.

* Update PanelContainerComponent unit test snapshot

* Fix commented code
2024-01-03 14:52:34 +01:00
85 changed files with 1685 additions and 5277 deletions

View File

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

View File

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

View File

@@ -2897,9 +2897,21 @@ a:link {
padding-left: 8px; padding-left: 8px;
} }
.settingsSectionInlineCheckbox {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
align-items: center;
gap: 5px;
}
.settingsSectionLabel { .settingsSectionLabel {
margin-bottom: @DefaultSpace; margin-bottom: @DefaultSpace;
margin-right: 5px; margin-right: 5px;
.panelInfoIcon {
margin-left: 5px;
}
} }
.pageOptionsPart { .pageOptionsPart {

View File

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

1424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.0", "@azure/cosmos": "4.0.1-beta.2",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.0.7",
@@ -55,8 +55,8 @@
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"d3": "6.1.1", "d3": "6.1.1",
"datatables.net-colreorder-dt": "1.5.1", "datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.10.19", "datatables.net-dt": "1.13.8",
"date-fns": "1.29.0", "date-fns": "1.29.0",
"dayjs": "1.8.19", "dayjs": "1.8.19",
"dom-to-image": "2.6.0", "dom-to-image": "2.6.0",
@@ -71,13 +71,14 @@
"iframe-resizer-react": "1.1.0", "iframe-resizer-react": "1.1.0",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.5.1", "jquery": "3.7.1",
"jquery-typeahead": "2.10.6", "jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.12.1", "jquery-ui-dist": "1.13.2",
"knockout": "3.5.1", "knockout": "3.5.1",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.44.0", "monaco-editor": "0.44.0",
"ms": "2.1.3", "ms": "2.1.3",
"patch-package": "8.0.0",
"p-retry": "4.6.2", "p-retry": "4.6.2",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42", "post-robot": "10.0.42",
@@ -114,11 +115,14 @@
"@types/codemirror": "0.0.56", "@types/codemirror": "0.0.56",
"@types/crossroads": "0.0.30", "@types/crossroads": "0.0.30",
"@types/d3": "5.9.2", "@types/d3": "5.9.2",
"@types/datatables.net": "1.10.28",
"@types/datatables.net-colreorder": "1.4.5",
"@types/dom-to-image": "2.6.2", "@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.7", "@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.6",
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/jquery": "3.5.29",
"@types/node": "12.11.1", "@types/node": "12.11.1",
"@types/post-robot": "10.0.1", "@types/post-robot": "10.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
@@ -187,6 +191,7 @@
"webpack-dev-server": "4.15.1" "webpack-dev-server": "4.15.1"
}, },
"scripts": { "scripts": {
"postinstall": "patch-package",
"start": "webpack serve --mode development", "start": "webpack serve --mode development",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build", "dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci", "build:dataExplorer:ci": "npm run build:ci",
@@ -233,4 +238,4 @@
"printWidth": 120, "printWidth": 120,
"endOfLine": "auto" "endOfLine": "auto"
} }
} }

View File

@@ -0,0 +1,22 @@
diff --git a/node_modules/datatables.net-colreorder/types/types.d.ts b/node_modules/datatables.net-colreorder/types/types.d.ts
index e5dc283..1930c2b 100644
--- a/node_modules/datatables.net-colreorder/types/types.d.ts
+++ b/node_modules/datatables.net-colreorder/types/types.d.ts
@@ -7,7 +7,7 @@
/// <reference types="jquery" />
-import DataTables, {Api} from 'datatables.net';
+import DataTables, { Api } from 'datatables.net';
export default DataTables;
@@ -40,6 +40,8 @@ declare module 'datatables.net' {
/**
* Create a new ColReorder instance for the target DataTable
*/
+ // Ignore this error: error TS7013: Construct signature, which lacks return-type annotation, implicitly has an 'any' return type.
+ // @ts-ignore
new (dt: Api<any>, settings: boolean | ConfigColReorder);
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
* https://github.com/running-coder/jquery-typeahead/issues/156 * https://github.com/running-coder/jquery-typeahead/issues/156
* TODO: Replace this minimum definition by the official one when it comes out. * TODO: Replace this minimum definition by the official one when it comes out.
*/ */
/// <reference path="jquery.d.ts" /> /// <reference types="jquery" />
interface JQueryTypeaheadParam { interface JQueryTypeaheadParam {
input: string; input: string;

View File

@@ -3,7 +3,7 @@
// Definitions by: Boris Yankov <https://github.com/borisyankov/>, John Reilly <https://github.com/johnnyreilly> // Definitions by: Boris Yankov <https://github.com/borisyankov/>, John Reilly <https://github.com/johnnyreilly>
// Definitions: https://github.com/borisyankov/DefinitelyTyped // Definitions: https://github.com/borisyankov/DefinitelyTyped
/// <reference path="jquery.d.ts"/> /// <reference types="jquery"/>
declare namespace JQueryUI { declare namespace JQueryUI {
// Accordion ////////////////////////////////////////////////// // Accordion //////////////////////////////////////////////////

File diff suppressed because it is too large Load Diff

View File

@@ -23,12 +23,12 @@ describe("ThroughputInput Pane", () => {
}); });
it("should switch mode properly", () => { it("should switch mode properly", () => {
wrapper.find('[aria-label="Manual database throughput"]').simulate("change"); wrapper.find('[id="Manual-input"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe( expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
"Container throughput (400 - unlimited RU/s)", "Container throughput (400 - unlimited RU/s)",
); );
wrapper.find('[aria-label="Autoscale database throughput"]').simulate("change"); wrapper.find('[id="Autoscale-input"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)"); expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
}); });
}); });

View File

@@ -189,7 +189,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<input <input
id="Autoscale-input" id="Autoscale-input"
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
aria-label="Autoscale database throughput" aria-label={`${getThroughputLabelText()} Autoscale`}
aria-required={true} aria-required={true}
checked={isAutoscaleSelected} checked={isAutoscaleSelected}
type="radio" type="radio"
@@ -204,7 +204,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<input <input
id="Manual-input" id="Manual-input"
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
aria-label="Manual database throughput" aria-label={`${getThroughputLabelText()} Manual`}
checked={!isAutoscaleSelected} checked={!isAutoscaleSelected}
type="radio" type="radio"
aria-required={true} aria-required={true}
@@ -276,6 +276,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Link> </Link>
. .
</Text> </Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
{isDatabase ? "Database" : getCollectionName()} Required RU/s
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.topLeftEdge} directionalHint={DirectionalHint.topLeftEdge}
@@ -296,7 +302,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400} min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity} max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={throughput.toString()} value={throughput.toString()}
aria-label="Max request units per second" ariaLabel={`${isDatabase ? "Database" : getCollectionName()} Required RU/s`}
required={true} required={true}
errorMessage={throughputError} errorMessage={throughputError}
/> />

View File

@@ -678,7 +678,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
role="radiogroup" role="radiogroup"
> >
<input <input
aria-label="Autoscale database throughput" aria-label="Container throughput (autoscale) Autoscale"
aria-required={true} aria-required={true}
checked={true} checked={true}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
@@ -695,7 +695,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
Autoscale Autoscale
</label> </label>
<input <input
aria-label="Manual database throughput" aria-label="Container throughput (autoscale) Manual"
aria-required={true} aria-required={true}
checked={false} checked={false}
className="throughputInputRadioBtn" className="throughputInputRadioBtn"

View File

@@ -3,10 +3,11 @@ import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { getCopilotEnabled } 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 { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
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";
@@ -264,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;
@@ -383,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;
} }
@@ -1389,9 +1362,18 @@ export default class Explorer {
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) { if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
return; return;
} }
const copilotEnabled = await getCopilotEnabled(); const copilotEnabledPromise = getCopilotEnabled();
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled); const copilotUserDBEnabledPromise = isCopilotFeatureRegistered(userContext.subscriptionId);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotEnabled); const [copilotEnabled, copilotUserDBEnabled] = await Promise.all([
copilotEnabledPromise,
copilotUserDBEnabledPromise,
]);
const copilotSampleDBEnabled = LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true";
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled && copilotUserDBEnabled);
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotUserDBEnabled);
useQueryCopilot
.getState()
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
} }
public async refreshSampleData(): Promise<void> { public async refreshSampleData(): Promise<void> {

View File

@@ -1163,15 +1163,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
)}"`, )}"`,
).then( ).then(
(documents: DataModels.DocumentId[]) => { (documents: DataModels.DocumentId[]) => {
$.each( $.each(documents, (index: number, doc: any) => {
documents, newIconsMap[doc["_graph_icon_property_value"]] = {
(index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => { data: doc["icon"],
newIconsMap[doc["_graph_icon_property_value"]] = { format: doc["format"],
data: doc["icon"], };
format: doc["format"], });
};
},
);
// Update graph configuration // Update graph configuration
this.setState({ this.setState({

View File

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

View File

@@ -135,7 +135,7 @@ export function createStaticCommandBarButtons(
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }
if (isQuerySupported && selectedNodeState.findSelectedCollection()) { if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
const openQueryBtn = createOpenQueryButton(container); const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()]; openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
buttons.push(openQueryBtn); buttons.push(openQueryBtn);
@@ -200,7 +200,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
{ {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: "Settings", iconAlt: "Settings",
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />), onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
commandButtonLabel: undefined, commandButtonLabel: undefined,
ariaLabel: "Settings", ariaLabel: "Settings",
tooltipText: "Settings", tooltipText: "Settings",

View File

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

View File

@@ -154,7 +154,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind.GlobalSettings || action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) { ) {
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />); useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={explorer} />);
} }
} }

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
}; };
} }
omponentDidMount(): void { componentDidMount(): void {
window.addEventListener("resize", () => this.setState({ height: this.getPanelHeight() })); window.addEventListener("resize", () => this.setState({ height: this.getPanelHeight() }));
} }
@@ -62,12 +62,12 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"} customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
headerClassName="panelHeader" headerClassName="panelHeader"
onRenderNavigationContent={this.props.onRenderNavigationContent} onRenderNavigationContent={this.props.onRenderNavigationContent}
isFooterAtBottom={true}
styles={{ styles={{
navigation: { borderBottom: "1px solid #cccccc" }, navigation: { borderBottom: "1px solid #cccccc" },
content: { padding: 0, height: "100%" }, content: { padding: 0 },
scrollableContent: { height: "100%" },
header: { padding: "0 0 8px 34px" }, header: { padding: "0 0 8px 34px" },
commands: { marginTop: 8 }, commands: { marginTop: 8, paddingTop: 0 },
}} }}
style={{ height: this.state.height }} style={{ height: this.state.height }}
> >

View File

@@ -6,7 +6,7 @@ import { SettingsPane } from "./SettingsPane";
describe("Settings Pane", () => { describe("Settings Pane", () => {
it("should render Default properly", () => { it("should render Default properly", () => {
const wrapper = shallow(<SettingsPane />); const wrapper = shallow(<SettingsPane explorer={null} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
@@ -18,7 +18,7 @@ describe("Settings Pane", () => {
}, },
} as DatabaseAccount, } as DatabaseAccount,
}); });
const wrapper = shallow(<SettingsPane />); const wrapper = shallow(<SettingsPane explorer={null} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });

View File

@@ -11,23 +11,38 @@ 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";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import Explorer from "../../Explorer";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export const SettingsPane: FunctionComponent = () => { export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
explorer,
}: {
explorer: Explorer;
}): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [refreshExplorer, setRefreshExplorer] = useState<boolean>(false);
const [pageOption, setPageOption] = useState<string>( const [pageOption, setPageOption] = useState<string>(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage
? 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),
); );
@@ -78,13 +93,17 @@ export const SettingsPane: FunctionComponent = () => {
? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel) ? LocalStorageUtility.getEntryString(StorageKey.PriorityLevel)
: Constants.PriorityLevel.Default, : Constants.PriorityLevel.Default,
); );
const [copilotSampleDBEnabled, setCopilotSampleDBEnabled] = useState<boolean>(
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
);
const explorerVersion = configContext.gitSha; const explorerVersion = configContext.gitSha;
const shouldShowQueryPageOptions = userContext.apiType === "SQL"; const shouldShowQueryPageOptions = userContext.apiType === "SQL";
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin"; const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin";
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin"; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin";
const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled(); const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled();
const handlerOnSubmit = () => { const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled;
const handlerOnSubmit = async () => {
setIsExecuting(true); setIsExecuting(true);
LocalStorageUtility.setEntryNumber( LocalStorageUtility.setEntryNumber(
@@ -92,6 +111,7 @@ export const SettingsPane: FunctionComponent = () => {
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);
@@ -100,6 +120,7 @@ export const SettingsPane: FunctionComponent = () => {
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString()); LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
if (shouldShowGraphAutoVizOption) { if (shouldShowGraphAutoVizOption) {
LocalStorageUtility.setEntryBoolean( LocalStorageUtility.setEntryBoolean(
@@ -108,6 +129,10 @@ export const SettingsPane: FunctionComponent = () => {
); );
} }
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(
@@ -139,6 +164,7 @@ export const SettingsPane: FunctionComponent = () => {
logConsoleInfo( logConsoleInfo(
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`, `Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
); );
refreshExplorer && (await explorer.refreshExplorer());
closeSidePanel(); closeSidePanel();
}; };
@@ -182,6 +208,17 @@ export const SettingsPane: FunctionComponent = () => {
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);
}; };
@@ -218,6 +255,12 @@ export const SettingsPane: FunctionComponent = () => {
} }
}; };
const handleSampleDatabaseChange = async (ev: React.MouseEvent<HTMLElement>, checked?: boolean): Promise<void> => {
setCopilotSampleDBEnabled(checked);
useQueryCopilot.getState().setCopilotSampleDBEnabled(checked);
setRefreshExplorer(false);
};
const choiceButtonStyles = { const choiceButtonStyles = {
root: { root: {
clear: "both", clear: "both",
@@ -240,7 +283,7 @@ export const SettingsPane: FunctionComponent = () => {
], ],
}; };
const queryTimeoutToggleStyles: IToggleStyles = { const toggleStyles: IToggleStyles = {
label: { label: {
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
@@ -253,7 +296,7 @@ export const SettingsPane: FunctionComponent = () => {
text: {}, text: {},
}; };
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = { const spinButtonStyles: ISpinButtonStyles = {
label: { label: {
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
@@ -319,48 +362,83 @@ export const SettingsPane: FunctionComponent = () => {
</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">
@@ -385,7 +463,7 @@ export const SettingsPane: FunctionComponent = () => {
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">
@@ -407,7 +485,7 @@ export const SettingsPane: FunctionComponent = () => {
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">
@@ -429,12 +507,12 @@ export const SettingsPane: FunctionComponent = () => {
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>
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel"> <div className="settingsSectionLabel">
Enable container pagination Enable container pagination
<InfoTooltip> <InfoTooltip>
@@ -454,7 +532,7 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
{shouldShowCrossPartitionOption && ( {shouldShowCrossPartitionOption && (
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel"> <div className="settingsSectionLabel">
Enable cross-partition query Enable cross-partition query
<InfoTooltip> <InfoTooltip>
@@ -545,6 +623,30 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
</div> </div>
)} )}
{shouldShowCopilotSampleDBOption && (
<div className="settingsSection">
<div className="settingsSectionPart settingsSectionInlineCheckbox">
<div className="settingsSectionLabel">
Enable sample database
<InfoTooltip>
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is
created by, and maintained by Microsoft at no cost to you.
</InfoTooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for Copilot"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
/>
</div>
</div>
)}
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
<div className="settingsSectionLabel">Explorer Version</div> <div className="settingsSectionLabel">Explorer Version</div>

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ exports[`PaneContainerComponent test should be resize if notification console is
customWidth="440px" customWidth="440px"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true}
isLightDismiss={true} isLightDismiss={true}
isOpen={true} isOpen={true}
onDismiss={[Function]} onDismiss={[Function]}
@@ -18,9 +19,9 @@ exports[`PaneContainerComponent test should be resize if notification console is
Object { Object {
"commands": Object { "commands": Object {
"marginTop": 8, "marginTop": 8,
"paddingTop": 0,
}, },
"content": Object { "content": Object {
"height": "100%",
"padding": 0, "padding": 0,
}, },
"header": Object { "header": Object {
@@ -29,9 +30,6 @@ exports[`PaneContainerComponent test should be resize if notification console is
"navigation": Object { "navigation": Object {
"borderBottom": "1px solid #cccccc", "borderBottom": "1px solid #cccccc",
}, },
"scrollableContent": Object {
"height": "100%",
},
} }
} }
type={7} type={7}
@@ -48,6 +46,7 @@ exports[`PaneContainerComponent test should render with panel content and header
customWidth="440px" customWidth="440px"
headerClassName="panelHeader" headerClassName="panelHeader"
headerText="test" headerText="test"
isFooterAtBottom={true}
isLightDismiss={true} isLightDismiss={true}
isOpen={true} isOpen={true}
onDismiss={[Function]} onDismiss={[Function]}
@@ -60,9 +59,9 @@ exports[`PaneContainerComponent test should render with panel content and header
Object { Object {
"commands": Object { "commands": Object {
"marginTop": 8, "marginTop": 8,
"paddingTop": 0,
}, },
"content": Object { "content": Object {
"height": "100%",
"padding": 0, "padding": 0,
}, },
"header": Object { "header": Object {
@@ -71,9 +70,6 @@ exports[`PaneContainerComponent test should render with panel content and header
"navigation": Object { "navigation": Object {
"borderBottom": "1px solid #cccccc", "borderBottom": "1px solid #cccccc",
}, },
"scrollableContent": Object {
"height": "100%",
},
} }
} }
type={7} type={7}

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ export const CopyPopup = ({
return showCopyPopup ? ( return showCopyPopup ? (
<Stack <Stack
role="status"
style={{ style={{
position: "fixed", position: "fixed",
width: 345, width: 345,

View File

@@ -4,6 +4,7 @@ exports[`Copy Popup snapshot test should render when showCopyPopup is false 1`]
exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = ` exports[`Copy Popup snapshot test should render when showCopyPopup is true 1`] = `
<Stack <Stack
role="status"
style={ style={
Object { Object {
"background": "#FFFFFF", "background": "#FFFFFF",

View File

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

View File

@@ -18,7 +18,6 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { HttpStatusCodes } from "Common/Constants"; import { HttpStatusCodes } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
@@ -71,7 +70,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
databaseId, databaseId,
containerId, containerId,
}: QueryCopilotPromptProps): JSX.Element => { }: QueryCopilotPromptProps): JSX.Element => {
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false); const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false);
const inputEdited = useRef(false); const inputEdited = useRef(false);
const { const {
openFeedbackModal, openFeedbackModal,
@@ -94,6 +93,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
setIsSamplePromptsOpen, setIsSamplePromptsOpen,
showSamplePrompts, showSamplePrompts,
setShowSamplePrompts, setShowSamplePrompts,
showPromptTeachingBubble,
setShowPromptTeachingBubble,
showDeletePopup, showDeletePopup,
setShowDeletePopup, setShowDeletePopup,
showFeedbackBar, showFeedbackBar,
@@ -272,16 +273,23 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}; };
const showTeachingBubble = (): void => { const showTeachingBubble = (): void => {
if (!inputEdited.current) { if (showPromptTeachingBubble && !inputEdited.current) {
setTimeout(() => { setTimeout(() => {
if (!inputEdited.current && !isWelcomModalVisible()) { if (!inputEdited.current && !isWelcomModalVisible()) {
toggleCopilotTeachingBubbleVisible(); setCopilotTeachingBubbleVisible(true);
inputEdited.current = true; inputEdited.current = true;
} }
}, 30000); }, 30000);
} else {
toggleCopilotTeachingBubbleVisible(false);
} }
}; };
const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => {
setCopilotTeachingBubbleVisible(visible);
setShowPromptTeachingBubble(visible);
};
const isWelcomModalVisible = (): boolean => { const isWelcomModalVisible = (): boolean => {
return localStorage.getItem("hideWelcomeModal") !== "true"; return localStorage.getItem("hideWelcomeModal") !== "true";
}; };
@@ -303,15 +311,29 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
resetButtonState(); resetButtonState();
}; };
const getAriaLabel = () => {
if (isGeneratingQuery === null) {
return " ";
} else if (isGeneratingQuery) {
return "Content is loading";
} else {
return "Content is updated";
}
};
React.useEffect(() => { React.useEffect(() => {
showTeachingBubble(); showTeachingBubble();
useTabs.getState().setIsQueryErrorThrown(false); useTabs.getState().setIsQueryErrorThrown(false);
}, []); }, []);
return ( return (
<Stack className="copilot-prompt-pane" styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}> <Stack
className="copilot-prompt-pane"
styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}
id="copilot-textfield-label"
>
<Stack horizontal> <Stack horizontal>
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} /> <Image src={CopilotIcon} style={{ width: 24, height: 24 }} alt="Copilot" role="none" />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text> <Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
<IconButton <IconButton
iconProps={{ imageProps: { src: errorIcon } }} iconProps={{ imageProps: { src: errorIcon } }}
@@ -326,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">
@@ -348,14 +371,15 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="off" autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you." placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label"
/> />
{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"
> >
@@ -363,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 }}
> >
@@ -377,8 +401,11 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
disabled={isGeneratingQuery || !userPrompt.trim()} disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
onClick={() => startGenerateQueryProcess()} onClick={() => startGenerateQueryProcess()}
aria-label="Send"
/> />
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />} <div role="alert" aria-label={getAriaLabel()}>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
</div>
{showSamplePrompts && ( {showSamplePrompts && (
<Callout <Callout
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }} styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
@@ -484,7 +511,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Stack style={{ margin: "8px 0" }}> <Stack style={{ margin: "8px 0" }}>
<Text style={{ fontSize: 12 }}> <Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "} AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank"> <Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072c9" }}>
Read preview terms Read preview terms
</Link> </Link>
{showErrorMessageBar && ( {showErrorMessageBar && (
@@ -516,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={() => {
@@ -551,10 +579,18 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<IconButton <IconButton
id="likeBtn" id="likeBtn"
style={{ marginLeft: 20 }} style={{ marginLeft: 20 }}
aria-label="Like"
role="toggle"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }} 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);
} }
@@ -562,16 +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;
}} }}
/> />
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
<Separator vertical style={{ color: "#EDEBE9" }} /> <Separator vertical style={{ color: "#EDEBE9" }} />
<CommandBarButton <CommandBarButton
onClick={copyGeneratedCode} onClick={copyGeneratedCode}

View File

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

View File

@@ -15,7 +15,12 @@ import { MinimalQueryIterator } from "Common/IteratorUtilities";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
import { ContainerConnectionInfo, CopilotEnabledConfiguration, IProvisionData } from "Contracts/DataModels"; import {
ContainerConnectionInfo,
CopilotEnabledConfiguration,
FeatureRegistration,
IProvisionData,
} from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels"; import { AuthorizationTokenHeaderMetadata, QueryResults } from "Contracts/ViewModels";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
@@ -52,6 +57,28 @@ async function fetchWithTimeout(
return response; return response;
} }
export const isCopilotFeatureRegistered = async (subscriptionId: string): Promise<boolean> => {
const api_version = "2021-07-01";
const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/MicrosoftCopilotForAzureInCDB?api-version=${api_version}`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
return false;
}
if (!response?.ok) {
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
return featureRegistration?.properties?.state === "Registered";
};
export const getCopilotEnabled = async (): Promise<boolean> => { export const getCopilotEnabled = async (): Promise<boolean> => {
const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`; const url = `${configContext.BACKEND_ENDPOINT}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();

View File

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

View File

@@ -49,7 +49,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
/> />
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={null}
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -74,7 +74,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
value="" value=""
/> />
<CustomizedIconButton <CustomizedIconButton
disabled={false} disabled={null}
iconProps={ iconProps={
Object { Object {
"iconName": "Send", "iconName": "Send",
@@ -134,7 +134,7 @@ exports[`Footer snapshot test should not pass text with non enter key 1`] = `
/> />
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={null}
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -159,7 +159,7 @@ exports[`Footer snapshot test should not pass text with non enter key 1`] = `
value="" value=""
/> />
<CustomizedIconButton <CustomizedIconButton
disabled={false} disabled={null}
iconProps={ iconProps={
Object { Object {
"iconName": "Send", "iconName": "Send",
@@ -219,7 +219,7 @@ exports[`Footer snapshot test should open sample prompts on button click 1`] = `
/> />
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={null}
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -244,7 +244,7 @@ exports[`Footer snapshot test should open sample prompts on button click 1`] = `
value="" value=""
/> />
<CustomizedIconButton <CustomizedIconButton
disabled={false} disabled={null}
iconProps={ iconProps={
Object { Object {
"iconName": "Send", "iconName": "Send",
@@ -304,7 +304,7 @@ exports[`Footer snapshot test should pass text with enter key 1`] = `
/> />
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={null}
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -329,7 +329,7 @@ exports[`Footer snapshot test should pass text with enter key 1`] = `
value="test message" value="test message"
/> />
<CustomizedIconButton <CustomizedIconButton
disabled={false} disabled={null}
iconProps={ iconProps={
Object { Object {
"iconName": "Send", "iconName": "Send",
@@ -389,7 +389,7 @@ exports[`Footer snapshot test should pass text with icon button 1`] = `
/> />
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={null}
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -414,7 +414,7 @@ exports[`Footer snapshot test should pass text with icon button 1`] = `
value="test message" value="test message"
/> />
<CustomizedIconButton <CustomizedIconButton
disabled={false} disabled={null}
iconProps={ iconProps={
Object { Object {
"iconName": "Send", "iconName": "Send",
@@ -474,7 +474,7 @@ exports[`Footer snapshot test should update user input 1`] = `
/> />
</Stack> </Stack>
<StyledTextFieldBase <StyledTextFieldBase
disabled={false} disabled={null}
multiline={true} multiline={true}
onChange={[Function]} onChange={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
@@ -499,7 +499,7 @@ exports[`Footer snapshot test should update user input 1`] = `
value="" value=""
/> />
<CustomizedIconButton <CustomizedIconButton
disabled={false} disabled={null}
iconProps={ iconProps={
Object { Object {
"iconName": "Send", "iconName": "Send",

View File

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

View File

@@ -148,23 +148,25 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
/> />
</Stack> </Stack>
<Stack horizontal tokens={{ childrenGap: 16 }}> <Stack horizontal tokens={{ childrenGap: 16 }}>
<SplashScreenButton {useQueryCopilot.getState().copilotEnabled && (
imgSrc={CopilotIcon} <SplashScreenButton
title={"Query faster with Copilot"} imgSrc={CopilotIcon}
description={ title={"Query faster with Copilot"}
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!" description={
} "Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
} }
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType }); onClick={() => {
}} const copilotVersion = userContext.features.copilotVersion;
/> if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
}
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/>
)}
<SplashScreenButton <SplashScreenButton
imgSrc={ConnectIcon} imgSrc={ConnectIcon}
title={"Connect"} title={"Connect"}

View File

@@ -1,6 +1,8 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as DataTable from "datatables.net-dt";
import loadingIndicator3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
@@ -94,7 +96,7 @@ function createDataTable(
}); });
} }
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTables.Settings>{ tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTable.Config>{
// WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText) // WARNING!!! SECURITY: If you add new columns, make sure you encode them if they are user strings from Azure (see encodeText)
// so that they don't get interpreted as HTML in our page. // so that they don't get interpreted as HTML in our page.
colReorder: true, colReorder: true,
@@ -116,7 +118,7 @@ function createDataTable(
sPrevious: "<", sPrevious: "<",
sLast: ">>", sLast: ">>",
}, },
sProcessing: '<img style="width: 28px; height: 6px; " src="images/LoadingIndicator_3Squares.gif">', sProcessing: `<img style="width: 28px; height: 6px; " src="${loadingIndicator3Squares}">`,
oAria: { oAria: {
sSortAscending: "", sSortAscending: "",
sSortDescending: "", sSortDescending: "",
@@ -345,7 +347,7 @@ function updateSelectionStatus(oSettings: any): void {
// TODO consider centralizing this "post-command" logic into some sort of Command Manager entity. // TODO consider centralizing this "post-command" logic into some sort of Command Manager entity.
// See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects." // See VSO:166520: "[Storage Explorer] Consider adding a 'command manager' to track command post-effects."
function updateDataTableFocus(queryTablesTabId: string): void { function updateDataTableFocus(queryTablesTabId: string): void {
var $activeElement: JQuery = $(document.activeElement); var $activeElement: JQuery<Element> = $(document.activeElement);
var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element. var isFocusLost: boolean = $activeElement.is("body"); // When focus is lost, "body" becomes the active element.
var storageExplorerFrameHasFocus: boolean = document.hasFocus(); var storageExplorerFrameHasFocus: boolean = document.hasFocus();
var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager; var operationManager = tableEntityListViewModelMap[queryTablesTabId].operationManager;

View File

@@ -1,3 +1,4 @@
import * as DataTable from "datatables.net-dt";
import * as Utilities from "../Utilities"; import * as Utilities from "../Utilities";
/** /**
@@ -8,7 +9,7 @@ import * as Utilities from "../Utilities";
* @param{$dataTableElem} JQuery data table element * @param{$dataTableElem} JQuery data table element
* @param{$settings} Settings to use when creating the data table * @param{$settings} Settings to use when creating the data table
*/ */
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable { export function createDataTable($dataTableElem: JQuery, settings: any): DataTable.Api<HTMLElement> {
return $dataTableElem.DataTable(applyDefaultRendering(settings)); return $dataTableElem.DataTable(applyDefaultRendering(settings));
} }
@@ -18,14 +19,14 @@ export function createDataTable($dataTableElem: JQuery, settings: any): DataTabl
* @param{settings} The settings to check * @param{settings} The settings to check
* @return The given settings with all columns having a rendering function * @return The given settings with all columns having a rendering function
*/ */
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy { function applyDefaultRendering(settings: DataTable.Config): any {
var tableColumns: DataTables.ColumnLegacy[] = null; var tableColumns: any[] = null;
if (settings.aoColumns) { if (settings.columns) {
tableColumns = settings.aoColumns; tableColumns = settings.columns;
} else if (settings.aoColumnDefs) { } else if (settings.columnDefs) {
// for tables we use aoColumnDefs instead of aoColumns // for tables we use aoColumnDefs instead of aoColumns
tableColumns = settings.aoColumnDefs; tableColumns = settings.columnDefs;
} }
// either the settings had no columns defined, or they were called // either the settings had no columns defined, or they were called

View File

@@ -1,11 +1,11 @@
import ko from "knockout"; import ko from "knockout";
import * as DataTableOperations from "./DataTableOperations";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import * as Entities from "../Entities";
import * as Utilities from "../Utilities";
import * as DataTableOperations from "./DataTableOperations";
import TableCommands from "./TableCommands"; import TableCommands from "./TableCommands";
import TableEntityListViewModel from "./TableEntityListViewModel"; import TableEntityListViewModel from "./TableEntityListViewModel";
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
/* /*
* Base class for data table row selection. * Base class for data table row selection.
@@ -13,9 +13,9 @@ import * as Entities from "../Entities";
export default class DataTableOperationManager { export default class DataTableOperationManager {
private _tableEntityListViewModel: TableEntityListViewModel; private _tableEntityListViewModel: TableEntityListViewModel;
private _tableCommands: TableCommands; private _tableCommands: TableCommands;
private dataTable: JQuery; private dataTable: JQuery<Element>;
constructor(table: JQuery, viewModel: TableEntityListViewModel, tableCommands: TableCommands) { constructor(table: JQuery<Element>, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
this.dataTable = table; this.dataTable = table;
this._tableEntityListViewModel = viewModel; this._tableEntityListViewModel = viewModel;
this._tableCommands = tableCommands; this._tableCommands = tableCommands;
@@ -25,7 +25,7 @@ export default class DataTableOperationManager {
} }
private click = (event: JQueryEventObject) => { private click = (event: JQueryEventObject) => {
var elem: JQuery = $(event.currentTarget); var elem: JQuery<Element> = $(event.currentTarget);
this.updateLastSelectedItem(elem, event.shiftKey); this.updateLastSelectedItem(elem, event.shiftKey);
if (Utilities.isEnvironmentCtrlPressed(event)) { if (Utilities.isEnvironmentCtrlPressed(event)) {
@@ -48,7 +48,7 @@ export default class DataTableOperationManager {
if (isUpArrowKey || isDownArrowKey) { if (isUpArrowKey || isDownArrowKey) {
var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem; var lastSelectedItem: Entities.ITableEntity = this._tableEntityListViewModel.lastSelectedItem;
var dataTableRows: JQuery = $(Constants.htmlSelectors.dataTableAllRowsSelector); var dataTableRows: JQuery<Element> = $(Constants.htmlSelectors.dataTableAllRowsSelector);
var maximumIndex = dataTableRows.length - 1; var maximumIndex = dataTableRows.length - 1;
// If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page. // If can't find an index for lastSelectedItem, then either no item is previously selected or it goes across page.
@@ -60,7 +60,7 @@ export default class DataTableOperationManager {
: -1; : -1;
var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1; var nextIndex: number = isUpArrowKey ? lastSelectedItemIndex - 1 : lastSelectedItemIndex + 1;
var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex); var safeIndex: number = Utilities.ensureBetweenBounds(nextIndex, 0, maximumIndex);
var selectedRowElement: JQuery = dataTableRows.eq(safeIndex); var selectedRowElement: JQuery<Element> = dataTableRows.eq(safeIndex);
if (selectedRowElement) { if (selectedRowElement) {
if (event.shiftKey) { if (event.shiftKey) {
@@ -143,13 +143,13 @@ export default class DataTableOperationManager {
return handled; return handled;
} }
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity { private getEntityIdentity($elem: JQuery<Element>): Entities.ITableEntityIdentity {
return { return {
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr), RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
}; };
} }
private updateLastSelectedItem($elem: JQuery, isShiftSelect: boolean) { private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
var entity = this._tableEntityListViewModel.getItemFromCurrentPage( var entity = this._tableEntityListViewModel.getItemFromCurrentPage(
this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey), this._tableEntityListViewModel.getTableEntityKeys(entityIdentity.RowKey),
@@ -162,7 +162,7 @@ export default class DataTableOperationManager {
} }
} }
private applySingleSelection($elem: JQuery) { private applySingleSelection($elem: JQuery<Element>) {
if ($elem) { if ($elem) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
@@ -179,7 +179,7 @@ export default class DataTableOperationManager {
); );
} }
private applyCtrlSelection($elem: JQuery): void { private applyCtrlSelection($elem: JQuery<Element>): void {
var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel var koSelected: ko.ObservableArray<Entities.ITableEntity> = this._tableEntityListViewModel
? this._tableEntityListViewModel.selected ? this._tableEntityListViewModel.selected
: null; : null;
@@ -200,7 +200,7 @@ export default class DataTableOperationManager {
} }
} }
private applyShiftSelection($elem: JQuery): void { private applyShiftSelection($elem: JQuery<Element>): void {
var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem; var anchorItem = this._tableEntityListViewModel.lastSelectedAnchorItem;
// If anchor item doesn't exist, use the first available item of current page instead // If anchor item doesn't exist, use the first available item of current page instead
@@ -228,7 +228,7 @@ export default class DataTableOperationManager {
} }
} }
private applyContextMenuSelection($elem: JQuery) { private applyContextMenuSelection($elem: JQuery<Element>) {
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem); var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
if ( if (

View File

@@ -1,3 +1,4 @@
import * as DataTables from "datatables.net";
import Q from "q"; import Q from "q";
import _ from "underscore"; import _ from "underscore";
import * as QueryBuilderConstants from "../Constants"; import * as QueryBuilderConstants from "../Constants";
@@ -13,7 +14,7 @@ export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector; return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
} }
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean { export function isRowVisible(dataTableScrollBodyQuery: JQuery<Element>, element: Element): boolean {
let isVisible = false; let isVisible = false;
if (dataTableScrollBodyQuery.length && element) { if (dataTableScrollBodyQuery.length && element) {
@@ -26,16 +27,18 @@ export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElem
return isVisible; return isVisible;
} }
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void { export function scrollToRowIfNeeded(dataTableRows: JQuery<Element>, currentIndex: number, isScrollUp: boolean): void {
if (dataTableRows.length) { if (dataTableRows.length) {
const dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector), const dataTableScrollBodyQuery: JQuery<Element> = $(
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex); QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector,
),
selectedRowElement: Element = dataTableRows.get(currentIndex);
if (dataTableScrollBodyQuery.length && selectedRowElement) { if (dataTableScrollBodyQuery.length && selectedRowElement) {
const isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement); const isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
if (!isVisible) { if (!isVisible) {
const selectedRowQuery: JQuery = $(selectedRowElement), const selectedRowQuery: JQuery<Element> = $(selectedRowElement),
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(), scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
selectedElementPosition: number = selectedRowQuery.position().top; selectedElementPosition: number = selectedRowQuery.position().top;
let newScrollPosition = 0; let newScrollPosition = 0;
@@ -54,8 +57,8 @@ export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number,
} }
export function scrollToTopIfNeeded(): void { export function scrollToTopIfNeeded(): void {
const $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector), const $dataTableRows: JQuery<Element> = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector); $dataTableScrollBody: JQuery<Element> = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
if ($dataTableRows.length && $dataTableScrollBody.length) { if ($dataTableRows.length && $dataTableScrollBody.length) {
$dataTableScrollBody.scrollTop(0); $dataTableScrollBody.scrollTop(0);
@@ -71,7 +74,7 @@ export function setPaginationButtonEventHandlers(): void {
.attr("role", "button"); .attr("role", "button");
} }
export function filterColumns(table: DataTables.DataTable, settings: boolean[]): void { export function filterColumns(table: DataTables.Api<HTMLElement>, settings: boolean[]): void {
settings && settings &&
settings.forEach((value: boolean, index: number) => { settings.forEach((value: boolean, index: number) => {
table.column(index).visible(value, false); table.column(index).visible(value, false);
@@ -84,7 +87,7 @@ export function filterColumns(table: DataTables.DataTable, settings: boolean[]):
* If no current order is specified, reorder the columns based on intial order. * If no current order is specified, reorder the columns based on intial order.
*/ */
export function reorderColumns( export function reorderColumns(
table: DataTables.DataTable, table: DataTables.Api<HTMLElement>,
targetOrder: number[], targetOrder: number[],
currentOrder?: number[], currentOrder?: number[],
//eslint-disable-next-line //eslint-disable-next-line
@@ -108,7 +111,9 @@ export function reorderColumns(
? calculateTransformationOrder(currentOrder, targetOrder) ? calculateTransformationOrder(currentOrder, targetOrder)
: targetOrder; : targetOrder;
try { try {
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder); // TODO: This possibly does not work with the new version of datatables.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
($.fn.dataTable as any).ColReorder(table).fnOrder(transformationOrder);
} catch (err) { } catch (err) {
return Q.reject(err); return Q.reject(err);
} }
@@ -116,9 +121,9 @@ export function reorderColumns(
return Q.resolve(null); return Q.resolve(null);
} }
export function resetColumns(table: DataTables.DataTable): void { // export function resetColumns(table: DataTables.DataTable): void {
$.fn.dataTable.ColReorder(table).fnReset(); // $.fn.dataTable.ColReorder(table).fnReset();
} // }
/** /**
* A table's initial order is described in the form of a natural ascending order. * A table's initial order is described in the form of a natural ascending order.
@@ -133,8 +138,10 @@ export function getInitialOrder(columnsCount: number): number[] {
* Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8} * Initial order: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8} * Current order: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
*/ */
export function getCurrentOrder(table: DataTables.DataTable): number[] { export function getCurrentOrder(table: DataTables.Api<HTMLElement>): number[] {
return $.fn.dataTable.ColReorder(table).fnOrder(); // TODO: This possibly does not work with the new version of datatables.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ($.fn.dataTable as any).ColReorder(table).fnOrder();
} }
/** /**
@@ -178,8 +185,8 @@ export function calculateTransformationOrder(currentOrder: number[], targetOrder
return transformationOrder; return transformationOrder;
} }
export function getDataTableHeaders(table: DataTables.DataTable): string[] { export function getDataTableHeaders(table: DataTables.Api<HTMLElement>): string[] {
const columns: DataTables.ColumnsMethods = table.columns(); const columns = table.columns();
let headers: string[] = []; let headers: string[] = [];
if (columns) { if (columns) {
// table.columns() return ColumnsMethods which is an array of arrays // table.columns() return ColumnsMethods which is an array of arrays

View File

@@ -1,14 +1,15 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import CacheBase from "./CacheBase"; import * as DataTables from "datatables.net";
import * as CommonConstants from "../../../Common/Constants"; import * as CommonConstants from "../../../Common/Constants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import CacheBase from "./CacheBase";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
// This is the format of the data we will have to pass to Datatable render callback, // This is the format of the data we will have to pass to Datatable render callback,
// and property names are defined by Datatable as well. // and property names are defined by Datatable as well.
@@ -27,7 +28,7 @@ abstract class DataTableViewModel {
public items = ko.observableArray<Entities.ITableEntity>(); public items = ko.observableArray<Entities.ITableEntity>();
public selected = ko.observableArray<Entities.ITableEntity>(); public selected = ko.observableArray<Entities.ITableEntity>();
public table: DataTables.DataTable; public table: DataTables.Api<HTMLElement>;
// The anchor item is for shift selection. i.e., select all items between anchor item and a give item. // The anchor item is for shift selection. i.e., select all items between anchor item and a give item.
public lastSelectedAnchorItem: Entities.ITableEntity; public lastSelectedAnchorItem: Entities.ITableEntity;

View File

@@ -1,3 +1,4 @@
import * as DataTables from "datatables.net";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
@@ -56,7 +57,7 @@ function _parse(err: any): ErrorDataModel[] {
function _getInnerErrors(message: string): any[] { function _getInnerErrors(message: string): any[] {
/* /*
The backend error message has an inner-message which is a stringified object. The backend error message has an inner-message which is a stringified object.
For SQL errors, the "errors" property is an array of SqlErrorDataModel. For SQL errors, the "errors" property is an array of SqlErrorDataModel.
Example: Example:
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p" "Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
@@ -131,7 +132,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }]; return [{ key: Constants.EntityKeyNames.RowKey, value: rowKey }];
} }
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable { public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.Api<Element> {
this.clearCache(); this.clearCache();
this.clearSelection(); this.clearSelection();
this.isCancelled = false; this.isCancelled = false;

View File

@@ -1,3 +1,4 @@
import * as DataTable from "datatables.net-dt";
import * as ko from "knockout"; import * as ko from "knockout";
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
@@ -643,7 +644,7 @@ export default class QueryBuilderViewModel {
return groupViewModels; return groupViewModels;
}; };
public runQuery = (): DataTables.DataTable => { public runQuery = (): DataTable.Api<Element> => {
return this._queryViewModel.runQuery(); return this._queryViewModel.runQuery();
}; };

View File

@@ -1,9 +1,10 @@
import * as DataTables from "datatables.net";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { getQuotedCqlIdentifier } from "../CqlUtilities"; import { getQuotedCqlIdentifier } from "../CqlUtilities";
@@ -158,7 +159,7 @@ export default class QueryViewModel {
notify: "always", notify: "always",
}); });
public runQuery = (): DataTables.DataTable => { public runQuery = (): DataTables.Api<Element> => {
let filter = this.setFilter(); let filter = this.setFilter();
if (filter && userContext.apiType !== "Cassandra") { if (filter && userContext.apiType !== "Cassandra") {
filter = filter.replace(/"/g, "'"); filter = filter.replace(/"/g, "'");
@@ -176,7 +177,7 @@ export default class QueryViewModel {
return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false); return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false);
}; };
public clearQuery = (): DataTables.DataTable => { public clearQuery = (): DataTables.Api<Element> => {
this.queryText(); this.queryText();
this.topValue(); this.topValue();
this.selectText(); this.selectText();

View File

@@ -281,7 +281,7 @@ export class CassandraAPIDataClient extends TableDataClient {
query, query,
paginationToken, paginationToken,
}, },
beforeSend: this.setAuthorizationHeader, beforeSend: this.setAuthorizationHeader as any,
cache: false, cache: false,
}); });
shouldNotify && shouldNotify &&
@@ -423,7 +423,7 @@ export class CassandraAPIDataClient extends TableDataClient {
keyspaceId: collection.databaseId, keyspaceId: collection.databaseId,
tableId: collection.id(), tableId: collection.id(),
}, },
beforeSend: this.setAuthorizationHeader, beforeSend: this.setAuthorizationHeader as any,
cache: false, cache: false,
}) })
.then( .then(
@@ -463,7 +463,7 @@ export class CassandraAPIDataClient extends TableDataClient {
keyspaceId: collection.databaseId, keyspaceId: collection.databaseId,
tableId: collection.id(), tableId: collection.id(),
}, },
beforeSend: this.setAuthorizationHeader, beforeSend: this.setAuthorizationHeader as any,
cache: false, cache: false,
}) })
.then( .then(
@@ -496,7 +496,7 @@ export class CassandraAPIDataClient extends TableDataClient {
resourceId: resourceId, resourceId: resourceId,
query: query, query: query,
}, },
beforeSend: this.setAuthorizationHeader, beforeSend: this.setAuthorizationHeader as any,
cache: false, cache: false,
}).then( }).then(
(data: any) => { (data: any) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -769,7 +769,10 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const dataRootNode = buildDataTree(); const dataRootNode = buildDataTree();
const isSampleDataEnabled = const isSampleDataEnabled =
useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL"; useQueryCopilot().copilotEnabled &&
useQueryCopilot().copilotSampleDBEnabled &&
userContext.sampleDataConnectionInfo &&
userContext.apiType === "SQL";
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection); const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import { ContainerInfo } from "../Contracts/DataModels";
export interface QueryCopilotState { export interface QueryCopilotState {
copilotEnabled: boolean; copilotEnabled: boolean;
copilotUserDBEnabled: boolean; copilotUserDBEnabled: boolean;
copilotSampleDBEnabled: boolean;
generatedQuery: string; generatedQuery: string;
likeQuery: boolean; likeQuery: boolean;
userPrompt: string; userPrompt: string;
@@ -28,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;
@@ -50,6 +52,7 @@ export interface QueryCopilotState {
setCopilotEnabled: (copilotEnabled: boolean) => void; setCopilotEnabled: (copilotEnabled: boolean) => void;
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void; setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => void;
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => void;
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void; openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => void;
closeFeedbackModal: () => void; closeFeedbackModal: () => void;
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void; setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) => void;
@@ -69,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;
@@ -91,11 +95,12 @@ 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,
copilotUserDBEnabled: false, copilotUserDBEnabled: false,
copilotSampleDBEnabled: false,
generatedQuery: "", generatedQuery: "",
likeQuery: false, likeQuery: false,
userPrompt: "", userPrompt: "",
@@ -104,7 +109,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
correlationId: "", correlationId: "",
query: "SELECT * FROM c", query: "SELECT * FROM c",
selectedQuery: "", selectedQuery: "",
isGeneratingQuery: false, isGeneratingQuery: null,
isGeneratingExplanation: false, isGeneratingExplanation: false,
isExecuting: false, isExecuting: false,
dislikeQuery: undefined, dislikeQuery: undefined,
@@ -145,6 +150,7 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }), setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }), setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => set({ copilotSampleDBEnabled }),
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) => openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }), set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }), closeFeedbackModal: () => set({ showFeedbackModal: false }),

View File

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

View File

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