mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 11:36:47 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b38f558248 | ||
|
|
3c9325ecbc | ||
|
|
309fcd112e | ||
|
|
eba617f53f |
@@ -145,5 +145,4 @@ 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
|
||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -20,8 +20,8 @@
|
|||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": true,
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": true
|
||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
|||||||
@@ -147,7 +147,6 @@
|
|||||||
|
|
||||||
// CommandBar
|
// CommandBar
|
||||||
@CommandBarButtonHeight: 40px;
|
@CommandBarButtonHeight: 40px;
|
||||||
@FabricCommandBarButtonHeight: 34px;
|
|
||||||
|
|
||||||
/**********************************************************************************
|
/**********************************************************************************
|
||||||
Portal Consts
|
Portal Consts
|
||||||
@@ -163,10 +162,9 @@
|
|||||||
/**********************************************************************************/
|
/**********************************************************************************/
|
||||||
|
|
||||||
@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;
|
||||||
@FabricToolbarIconColor: "brightness(0) saturate(100%) invert(50%) sepia(17%) saturate(1459%) hue-rotate(81deg) brightness(99%) contrast(94%)";
|
|
||||||
|
|
||||||
@FabricBoxBorderRadius: 8px;
|
@FabricBoxBorderRadius: 8px;
|
||||||
@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;
|
@FabricBoxBorderShadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.14);
|
||||||
@FabricBoxMargin: 4px 3px 4px 3px;
|
@FabricBoxMargin: 4px 3px 4px 3px;
|
||||||
|
|
||||||
@FabricAccentMediumHigh: #0c695a;
|
@FabricAccentMediumHigh: #0c695a;
|
||||||
|
|||||||
@@ -2897,21 +2897,9 @@ 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 {
|
||||||
|
|||||||
@@ -25,38 +25,33 @@ a:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.resourceTreeAndTabs {
|
.resourceTreeAndTabs {
|
||||||
border-radius: 0px;
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
margin-top: 4px;
|
||||||
margin-bottom: 0px;
|
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabsManagerContainer {
|
.tabsManagerContainer {
|
||||||
background-color: #ffffff
|
background-color: #fafafa
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs-margin {
|
.nav-tabs-margin {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
background-color: #ffffff
|
background-color: #fafafa
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandBarContainer {
|
.commandBarContainer {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
|
border-bottom: none;
|
||||||
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
|
||||||
margin-bottom: 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 {
|
||||||
@@ -163,10 +158,9 @@ a:focus {
|
|||||||
|
|
||||||
|
|
||||||
.dataExplorerErrorConsoleContainer {
|
.dataExplorerErrorConsoleContainer {
|
||||||
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
|
border-radius: @FabricBoxBorderRadius;
|
||||||
box-shadow: @FabricBoxBorderShadow;
|
box-shadow: @FabricBoxBorderShadow;
|
||||||
margin: @FabricBoxMargin;
|
margin: @FabricBoxMargin;
|
||||||
margin-top: 0px;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
align-self: auto;
|
align-self: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
1430
package-lock.json
generated
1430
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -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.1-beta.2",
|
"@azure/cosmos": "4.0.0",
|
||||||
"@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.7.0",
|
"datatables.net-colreorder-dt": "1.5.1",
|
||||||
"datatables.net-dt": "1.13.8",
|
"datatables.net-dt": "1.10.19",
|
||||||
"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,15 +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.7.1",
|
"jquery": "3.5.1",
|
||||||
"jquery-typeahead": "2.11.1",
|
"jquery-typeahead": "2.10.6",
|
||||||
"jquery-ui-dist": "1.13.2",
|
"jquery-ui-dist": "1.12.1",
|
||||||
"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",
|
||||||
"p-retry": "4.6.2",
|
"p-retry": "4.6.2",
|
||||||
"patch-package": "8.0.0",
|
|
||||||
"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",
|
||||||
"q": "1.5.1",
|
"q": "1.5.1",
|
||||||
@@ -115,14 +114,11 @@
|
|||||||
"@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",
|
||||||
@@ -191,7 +187,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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);
|
|
||||||
|
|
||||||
/**
|
|
||||||
@@ -211,10 +211,6 @@ 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;
|
||||||
@@ -435,22 +431,6 @@ export class JunoEndpoints {
|
|||||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
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 class CassandraProxyEndpoints {
|
|
||||||
public static readonly Development: string = "https://localhost:7240";
|
|
||||||
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
|
|
||||||
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
|
|
||||||
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
|
|
||||||
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PriorityLevel {
|
export class PriorityLevel {
|
||||||
public static readonly High = "high";
|
public static readonly High = "high";
|
||||||
public static readonly Low = "low";
|
public static readonly Low = "low";
|
||||||
|
|||||||
@@ -40,10 +40,9 @@ 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.fabricContext.databaseConnectionInfo.resourceTokens;
|
const resourceTokens = userContext.fabricDatabaseConnectionInfo.resourceTokens;
|
||||||
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
checkDatabaseResourceTokensValidity(userContext.fabricDatabaseConnectionInfo.resourceTokensTimestamp);
|
||||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||||
|
|
||||||
case Cosmos.ResourceType.none:
|
case Cosmos.ResourceType.none:
|
||||||
@@ -52,11 +51,9 @@ 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>(
|
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(MessageTypes.GetAuthorizationToken, [
|
||||||
MessageTypes.GetAuthorizationToken,
|
requestInfo,
|
||||||
[requestInfo],
|
]);
|
||||||
userContext.fabricContext.connectionId,
|
|
||||||
);
|
|
||||||
console.log("Response from Fabric: ", authorizationToken);
|
console.log("Response from Fabric: ", authorizationToken);
|
||||||
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
headers[HttpHeaders.msDate] = authorizationToken.XDate;
|
||||||
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { QueryOperationOptions } from "@azure/cosmos";
|
|
||||||
import { QueryResults } from "../Contracts/ViewModels";
|
import { QueryResults } from "../Contracts/ViewModels";
|
||||||
|
|
||||||
interface QueryResponse {
|
interface QueryResponse {
|
||||||
@@ -11,17 +10,13 @@ interface QueryResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MinimalQueryIterator {
|
export interface MinimalQueryIterator {
|
||||||
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
|
fetchNext: () => Promise<QueryResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick<QueryIterator<any>, "fetchNext">;
|
// Pick<QueryIterator<any>, "fetchNext">;
|
||||||
|
|
||||||
export function nextPage(
|
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
|
||||||
documentsIterator: MinimalQueryIterator,
|
return documentsIterator.fetchNext().then((response) => {
|
||||||
firstItemIndex: number,
|
|
||||||
queryOperationOptions?: QueryOperationOptions,
|
|
||||||
): Promise<QueryResults> {
|
|
||||||
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
|
||||||
const documents = response.resources;
|
const documents = response.resources;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
|
||||||
|
|||||||
@@ -27,24 +27,15 @@ 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(scope),
|
id: _.uniqueId(),
|
||||||
};
|
};
|
||||||
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
RequestMap[cachedDataPromise.id] = cachedDataPromise;
|
||||||
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
sendMessage({ type: messageType, params: params, id: cachedDataPromise.id });
|
||||||
@@ -56,10 +47,6 @@ export function sendCachedDataMessage<TResponseDataModel>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param data Overwrite the data property of the message
|
|
||||||
*/
|
|
||||||
export function sendMessage(data: any): void {
|
export function sendMessage(data: any): void {
|
||||||
_sendMessage({
|
_sendMessage({
|
||||||
signature: "pcIframe",
|
signature: "pcIframe",
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
import {
|
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
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";
|
||||||
@@ -14,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, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
|
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -66,73 +62,6 @@ 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;
|
||||||
@@ -193,54 +122,6 @@ 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;
|
||||||
@@ -288,51 +169,6 @@ 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;
|
||||||
@@ -372,56 +208,6 @@ 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;
|
||||||
@@ -451,7 +237,7 @@ export function updateDocument_ToBeDeprecated(
|
|||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
...authHeaders(),
|
...authHeaders(),
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
[HttpHeaders.contentType]: "application/json",
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -464,53 +250,6 @@ export function updateDocument_ToBeDeprecated(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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")) {
|
|
||||||
return 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("/");
|
||||||
@@ -538,7 +277,7 @@ export function deleteDocument_ToBeDeprecated(
|
|||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
...authHeaders(),
|
...authHeaders(),
|
||||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
[HttpHeaders.contentType]: "application/json",
|
||||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -552,52 +291,6 @@ export function deleteDocument_ToBeDeprecated(
|
|||||||
|
|
||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
|
||||||
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
|
|
||||||
return 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];
|
||||||
@@ -641,20 +334,13 @@ export function createMongoCollectionWithProxy_ToBeDeprecated(
|
|||||||
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 {
|
||||||
let endpoint;
|
const endpoint =
|
||||||
if (useMongoProxyEndpoint(feature)) {
|
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||||
} else {
|
? userContext.features.mongoProxyEndpoint
|
||||||
endpoint =
|
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
|
||||||
validateEndpoint(userContext.features.mongoProxyEndpoint, [
|
|
||||||
...allowedMongoProxyEndpoints,
|
|
||||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
|
||||||
])
|
|
||||||
? userContext.features.mongoProxyEndpoint
|
|
||||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getEndpoint(endpoint);
|
return getEndpoint(endpoint);
|
||||||
}
|
}
|
||||||
@@ -663,11 +349,7 @@ 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) {
|
||||||
if (endpoint === configContext.MONGO_PROXY_ENDPOINT) {
|
url = url.replace("api/mongo", "api/guest/mongo");
|
||||||
url = url.replace("api/mongo", "api/connectionstring/mongo");
|
|
||||||
} else {
|
|
||||||
url = url.replace("api/mongo", "api/guest/mongo");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -688,16 +370,3 @@ 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 {
|
|
||||||
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
|
|
||||||
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
|
|
||||||
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
canAccessMongoProxy &&
|
|
||||||
configContext.NEW_MONGO_APIS?.includes(api) &&
|
|
||||||
[MongoProxyEndpoints.Development, MongoProxyEndpoints.Mpac].includes(configContext.MONGO_PROXY_ENDPOINT)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import { ApiType, userContext } from "UserContext";
|
|
||||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
|
||||||
import {
|
|
||||||
cancel,
|
|
||||||
create,
|
|
||||||
get,
|
|
||||||
listByDatabaseAccount,
|
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
|
|
||||||
import {
|
|
||||||
CosmosCassandraDataTransferDataSourceSink,
|
|
||||||
CosmosMongoDataTransferDataSourceSink,
|
|
||||||
CosmosSqlDataTransferDataSourceSink,
|
|
||||||
CreateJobRequest,
|
|
||||||
DataTransferJobFeedResults,
|
|
||||||
DataTransferJobGetResults,
|
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/types";
|
|
||||||
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
|
||||||
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
|
|
||||||
|
|
||||||
export interface DataTransferParams {
|
|
||||||
jobName: string;
|
|
||||||
apiType: ApiType;
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroupName: string;
|
|
||||||
accountName: string;
|
|
||||||
sourceDatabaseName: string;
|
|
||||||
sourceCollectionName: string;
|
|
||||||
targetDatabaseName: string;
|
|
||||||
targetCollectionName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDataTransferJobs = async (
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroup: string,
|
|
||||||
accountName: string,
|
|
||||||
): Promise<DataTransferJobGetResults[]> => {
|
|
||||||
let dataTransferJobs: DataTransferJobGetResults[] = [];
|
|
||||||
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroup,
|
|
||||||
accountName,
|
|
||||||
);
|
|
||||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
|
||||||
while (dataTransferFeeds?.nextLink) {
|
|
||||||
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
|
|
||||||
headers: {
|
|
||||||
Authorization: userContext.authorizationToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (nextResponse.ok) {
|
|
||||||
dataTransferFeeds = await nextResponse.json();
|
|
||||||
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataTransferJobs;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initiateDataTransfer = async (params: DataTransferParams): Promise<DataTransferJobGetResults> => {
|
|
||||||
const {
|
|
||||||
jobName,
|
|
||||||
apiType,
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroupName,
|
|
||||||
accountName,
|
|
||||||
sourceDatabaseName,
|
|
||||||
sourceCollectionName,
|
|
||||||
targetDatabaseName,
|
|
||||||
targetCollectionName,
|
|
||||||
} = params;
|
|
||||||
const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName);
|
|
||||||
const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName);
|
|
||||||
const body: CreateJobRequest = {
|
|
||||||
properties: {
|
|
||||||
source: sourcePayload,
|
|
||||||
destination: targetPayload,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return create(subscriptionId, resourceGroupName, accountName, jobName, body);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pollDataTransferJob = async (
|
|
||||||
jobName: string,
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
): Promise<unknown> => {
|
|
||||||
const currentPollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
|
|
||||||
if (currentPollingJobs.has(jobName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let clearMessage = NotificationConsoleUtils.logConsoleProgress(`Data transfer job ${jobName} in progress`);
|
|
||||||
return await promiseRetry(
|
|
||||||
() => pollDataTransferJobOperation(jobName, subscriptionId, resourceGroupName, accountName, clearMessage),
|
|
||||||
{
|
|
||||||
retries: 500,
|
|
||||||
maxTimeout: 5000,
|
|
||||||
onFailedAttempt: (error: FailedAttemptError) => {
|
|
||||||
clearMessage();
|
|
||||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(error.message);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pollDataTransferJobOperation = async (
|
|
||||||
jobName: string,
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
clearMessage?: () => void,
|
|
||||||
): Promise<DataTransferJobGetResults> => {
|
|
||||||
if (!userContext.authorizationToken) {
|
|
||||||
throw new Error("No authority token provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
addToPolling(jobName);
|
|
||||||
|
|
||||||
const body: DataTransferJobGetResults = await get(subscriptionId, resourceGroupName, accountName, jobName);
|
|
||||||
const status = body?.properties?.status;
|
|
||||||
|
|
||||||
updateDataTransferJob(body);
|
|
||||||
|
|
||||||
if (status === "Cancelled" || status === "Failed" || status === "Faulted") {
|
|
||||||
removeFromPolling(jobName);
|
|
||||||
const errorMessage = body?.properties?.error
|
|
||||||
? JSON.stringify(body?.properties?.error)
|
|
||||||
: "Operation could not be completed";
|
|
||||||
const error = new Error(errorMessage);
|
|
||||||
clearMessage && clearMessage();
|
|
||||||
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} Failed`);
|
|
||||||
throw new AbortError(error);
|
|
||||||
}
|
|
||||||
if (status === "Completed") {
|
|
||||||
removeFromPolling(jobName);
|
|
||||||
clearMessage && clearMessage();
|
|
||||||
NotificationConsoleUtils.logConsoleInfo(`Data transfer job ${jobName} completed`);
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
const processedCount = body.properties.processedCount;
|
|
||||||
const totalCount = body.properties.totalCount;
|
|
||||||
const retryMessage = `Data transfer job ${jobName} in progress, total count: ${totalCount}, processed count: ${processedCount}`;
|
|
||||||
throw new Error(retryMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelDataTransferJob = async (
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
jobName: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
const cancelResult: DataTransferJobGetResults = await cancel(subscriptionId, resourceGroupName, accountName, jobName);
|
|
||||||
updateDataTransferJob(cancelResult);
|
|
||||||
removeFromPolling(cancelResult?.properties?.jobName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPayload = (
|
|
||||||
apiType: ApiType,
|
|
||||||
databaseName: string,
|
|
||||||
containerName: string,
|
|
||||||
):
|
|
||||||
| CosmosSqlDataTransferDataSourceSink
|
|
||||||
| CosmosMongoDataTransferDataSourceSink
|
|
||||||
| CosmosCassandraDataTransferDataSourceSink => {
|
|
||||||
switch (apiType) {
|
|
||||||
case "SQL":
|
|
||||||
return {
|
|
||||||
component: "CosmosDBSql",
|
|
||||||
databaseName: databaseName,
|
|
||||||
containerName: containerName,
|
|
||||||
} as CosmosSqlDataTransferDataSourceSink;
|
|
||||||
case "Mongo":
|
|
||||||
return {
|
|
||||||
component: "CosmosDBMongo",
|
|
||||||
databaseName: databaseName,
|
|
||||||
collectionName: containerName,
|
|
||||||
} as CosmosMongoDataTransferDataSourceSink;
|
|
||||||
case "Cassandra":
|
|
||||||
return {
|
|
||||||
component: "CosmosDBCassandra",
|
|
||||||
keyspaceName: databaseName,
|
|
||||||
tableName: containerName,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported API type for data transfer: ${apiType}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -9,13 +8,12 @@ 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, queryOperationOptions);
|
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||||
const itemCount = (result.documents && result.documents.length) || 0;
|
const itemCount = (result.documents && result.documents.length) || 0;
|
||||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
configContext.platform === Platform.Fabric &&
|
configContext.platform === Platform.Fabric &&
|
||||||
userContext.fabricContext &&
|
userContext.fabricDatabaseConnectionInfo &&
|
||||||
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
userContext.fabricDatabaseConnectionInfo.databaseId === databaseId
|
||||||
) {
|
) {
|
||||||
const collections: DataModels.Collection[] = [];
|
const collections: DataModels.Collection[] = [];
|
||||||
const promises: Promise<ContainerResponse>[] = [];
|
const promises: Promise<ContainerResponse>[] = [];
|
||||||
|
|
||||||
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
for (const collectionResourceId in userContext.fabricDatabaseConnectionInfo.resourceTokens) {
|
||||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||||
const resourceIdObj = collectionResourceId.split("/");
|
const resourceIdObj = collectionResourceId.split("/");
|
||||||
const tokenDatabaseId = resourceIdObj[1];
|
const tokenDatabaseId = resourceIdObj[1];
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
let databases: DataModels.Database[];
|
let databases: DataModels.Database[];
|
||||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
|
||||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricDatabaseConnectionInfo?.resourceTokens) {
|
||||||
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
const tokensData = userContext.fabricDatabaseConnectionInfo;
|
||||||
|
|
||||||
const databaseIdsSet = new Set<string>(); // databaseId
|
const databaseIdsSet = new Set<string>(); // databaseId
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { JunoEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
allowedAadEndpoints,
|
allowedAadEndpoints,
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedCassandraProxyEndpoints,
|
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMongoBackendEndpoints,
|
allowedMongoBackendEndpoints,
|
||||||
allowedMongoProxyEndpoints,
|
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointValidation";
|
||||||
|
|
||||||
export enum Platform {
|
export enum Platform {
|
||||||
Portal = "Portal",
|
Portal = "Portal",
|
||||||
@@ -40,10 +38,6 @@ 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;
|
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
|
|
||||||
NEW_MONGO_APIS?: string[];
|
|
||||||
CASSANDRA_PROXY_ENDPOINT?: string;
|
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
@@ -88,17 +82,6 @@ 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: MongoProxyEndpoints.Prod,
|
|
||||||
NEW_MONGO_APIS: [
|
|
||||||
// "resourcelist",
|
|
||||||
// "createDocument",
|
|
||||||
// "readDocument",
|
|
||||||
// "updateDocument",
|
|
||||||
// "deleteDocument",
|
|
||||||
// "createCollectionWithProxy",
|
|
||||||
],
|
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
|
||||||
isTerminalEnabled: false,
|
isTerminalEnabled: false,
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
};
|
};
|
||||||
@@ -144,18 +127,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
|
|
||||||
delete newContext.CASSANDRA_PROXY_ENDPOINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||||
delete newContext.JUNO_ENDPOINT;
|
delete newContext.JUNO_ENDPOINT;
|
||||||
}
|
}
|
||||||
@@ -173,7 +148,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
|
|
||||||
// Injected for local development. These will be removed in the production bundle by webpack
|
// Injected for local development. These will be removed in the production bundle by webpack
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
const port: string = process.env.PORT || "1234";
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||||
|
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
|
||||||
PROXY_PATH: "/proxy",
|
PROXY_PATH: "/proxy",
|
||||||
EMULATOR_ENDPOINT: "https://localhost:8081",
|
EMULATOR_ENDPOINT: "https://localhost:8081",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { MessageTypes } from "./MessageTypes";
|
|
||||||
|
|
||||||
// This is the current version of these messages
|
|
||||||
export const DATA_EXPLORER_RPC_VERSION = "2";
|
|
||||||
|
|
||||||
// Data Explorer to Fabric
|
|
||||||
|
|
||||||
// TODO Remove when upgrading to Fabric v2
|
|
||||||
export type DataExploreMessageV1 =
|
|
||||||
| "ready"
|
|
||||||
| {
|
|
||||||
type: MessageTypes.GetAuthorizationToken;
|
|
||||||
id: string;
|
|
||||||
params: GetCosmosTokenMessageOptions[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: MessageTypes.GetAllResourceTokens;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
export type DataExploreMessageV2 =
|
|
||||||
| {
|
|
||||||
type: MessageTypes.Ready;
|
|
||||||
id: string;
|
|
||||||
params: [string]; // version
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: MessageTypes.GetAuthorizationToken;
|
|
||||||
id: string;
|
|
||||||
params: GetCosmosTokenMessageOptions[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: MessageTypes.GetAllResourceTokens;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetCosmosTokenMessageOptions = {
|
|
||||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
|
||||||
resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges";
|
|
||||||
resourceId: string;
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import * as ActionContracts from "./ActionContracts";
|
import * as ActionContracts from "./ActionContracts";
|
||||||
import * as Diagnostics from "./Diagnostics";
|
import * as Diagnostics from "./Diagnostics";
|
||||||
import { MessageTypes } from "./MessageTypes";
|
|
||||||
import * as Versions from "./Versions";
|
import * as Versions from "./Versions";
|
||||||
|
|
||||||
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
export { ActionContracts, Diagnostics, MessageTypes, Versions };
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { AuthorizationToken } from "./MessageTypes";
|
import { AuthorizationToken, MessageTypes } from "./MessageTypes";
|
||||||
|
|
||||||
// This is the version of these messages
|
export type FabricMessage =
|
||||||
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;
|
||||||
@@ -32,52 +26,38 @@ export type FabricMessageV1 =
|
|||||||
| {
|
| {
|
||||||
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 FabricMessageV2 =
|
export type DataExploreMessage =
|
||||||
|
| "ready"
|
||||||
| {
|
| {
|
||||||
type: "newContainer";
|
type: MessageTypes.TelemetryInfo;
|
||||||
databaseName: string;
|
data: {
|
||||||
|
action: "LoadDatabases";
|
||||||
|
actionModifier: "success" | "start";
|
||||||
|
defaultExperience: "SQL";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "initialize";
|
type: MessageTypes.GetAuthorizationToken;
|
||||||
version: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
message: {
|
params: GetCosmosTokenMessageOptions[];
|
||||||
connectionId: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "authorizationToken";
|
type: MessageTypes.GetAllResourceTokens;
|
||||||
message: {
|
|
||||||
id: string;
|
|
||||||
error: string | undefined;
|
|
||||||
data: AuthorizationToken | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
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;
|
||||||
@@ -86,9 +66,12 @@ export type CosmosDBTokenResponse = {
|
|||||||
export type CosmosDBConnectionInfoResponse = {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
resourceTokens: { [resourceId: string]: string };
|
resourceTokens: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
export interface FabricDatabaseConnectionInfo {
|
||||||
|
endpoint: string;
|
||||||
|
databaseId: string;
|
||||||
|
resourceTokens: { [resourceId: string]: string };
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 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,
|
||||||
@@ -43,10 +37,10 @@ export enum MessageTypes {
|
|||||||
DisplayNPSSurvey,
|
DisplayNPSSurvey,
|
||||||
OpenVCoreMongoNetworkingBlade,
|
OpenVCoreMongoNetworkingBlade,
|
||||||
OpenVCoreMongoConnectionStringsBlade,
|
OpenVCoreMongoConnectionStringsBlade,
|
||||||
GetAuthorizationToken, // Data Explorer -> Fabric
|
|
||||||
GetAllResourceTokens, // Data Explorer -> Fabric
|
// Data Explorer -> Fabric communication
|
||||||
Ready, // Data Explorer -> Fabric
|
GetAuthorizationToken,
|
||||||
OpenCESCVAFeedbackBlade,
|
GetAllResourceTokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthorizationToken {
|
export interface AuthorizationToken {
|
||||||
|
|||||||
@@ -386,10 +386,9 @@ export interface DataExplorerInputsFrame {
|
|||||||
dnsSuffix?: string;
|
dnsSuffix?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
extensionEndpoint?: string;
|
extensionEndpoint?: string;
|
||||||
mongoProxyEndpoint?: string;
|
|
||||||
cassandraProxyEndpoint?: string;
|
|
||||||
subscriptionType?: SubscriptionType;
|
subscriptionType?: SubscriptionType;
|
||||||
quotaId?: string;
|
quotaId?: string;
|
||||||
|
addCollectionDefaultFlight?: string;
|
||||||
isTryCosmosDBSubscription?: boolean;
|
isTryCosmosDBSubscription?: boolean;
|
||||||
loadDatabaseAccountTimestamp?: number;
|
loadDatabaseAccountTimestamp?: number;
|
||||||
sharedThroughputMinimum?: number;
|
sharedThroughputMinimum?: number;
|
||||||
|
|||||||
1954
src/Definitions/datatables.d.ts
vendored
Normal file
1954
src/Definitions/datatables.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
src/Definitions/jquery-typescript.d.ts
vendored
2
src/Definitions/jquery-typescript.d.ts
vendored
@@ -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 types="jquery" />
|
/// <reference path="jquery.d.ts" />
|
||||||
|
|
||||||
interface JQueryTypeaheadParam {
|
interface JQueryTypeaheadParam {
|
||||||
input: string;
|
input: string;
|
||||||
|
|||||||
2
src/Definitions/jquery-ui.d.ts
vendored
2
src/Definitions/jquery-ui.d.ts
vendored
@@ -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 types="jquery"/>
|
/// <reference path="jquery.d.ts"/>
|
||||||
|
|
||||||
declare namespace JQueryUI {
|
declare namespace JQueryUI {
|
||||||
// Accordion //////////////////////////////////////////////////
|
// Accordion //////////////////////////////////////////////////
|
||||||
|
|||||||
1890
src/Definitions/jquery.d.ts
vendored
Normal file
1890
src/Definitions/jquery.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
import { useDatabases } from "Explorer/useDatabases";
|
||||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
|
||||||
import * as React from "react";
|
import * as React 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";
|
||||||
@@ -19,10 +18,6 @@ import { userContext } from "../../../UserContext";
|
|||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import {
|
|
||||||
PartitionKeyComponent,
|
|
||||||
PartitionKeyComponentProps,
|
|
||||||
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
|
|
||||||
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import "./SettingsComponent.less";
|
import "./SettingsComponent.less";
|
||||||
@@ -133,7 +128,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
private shouldShowIndexingPolicyEditor: boolean;
|
private shouldShowIndexingPolicyEditor: boolean;
|
||||||
private shouldShowPartitionKeyEditor: boolean;
|
|
||||||
private totalThroughputUsed: number;
|
private totalThroughputUsed: number;
|
||||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||||
|
|
||||||
@@ -146,7 +140,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.offer = this.collection?.offer();
|
this.offer = this.collection?.offer();
|
||||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||||
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
|
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||||
|
|
||||||
@@ -1063,12 +1056,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
|
||||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
|
||||||
collection: this.collection,
|
|
||||||
explorer: this.props.settingsTab.getContainer(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
@@ -1104,13 +1091,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowPartitionKeyEditor) {
|
|
||||||
tabs.push({
|
|
||||||
tab: SettingsV2TabTypes.PartitionKeyTab,
|
|
||||||
content: <PartitionKeyComponent {...partitionKeyComponentProps} />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
import {
|
|
||||||
DefaultButton,
|
|
||||||
FontWeights,
|
|
||||||
Link,
|
|
||||||
MessageBar,
|
|
||||||
MessageBarType,
|
|
||||||
PrimaryButton,
|
|
||||||
ProgressIndicator,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
|
||||||
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
|
||||||
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
|
||||||
import {
|
|
||||||
CosmosSqlDataTransferDataSourceSink,
|
|
||||||
DataTransferJobGetResults,
|
|
||||||
} from "Utils/arm/generatedClients/dataTransferService/types";
|
|
||||||
import { refreshDataTransferJobs, useDataTransferJobs } from "hooks/useDataTransferJobs";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import { userContext } from "../../../../UserContext";
|
|
||||||
|
|
||||||
export interface PartitionKeyComponentProps {
|
|
||||||
database: ViewModels.Database;
|
|
||||||
collection: ViewModels.Collection;
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
|
|
||||||
const { dataTransferJobs } = useDataTransferJobs();
|
|
||||||
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const loadDataTransferJobs = refreshDataTransferOperations;
|
|
||||||
loadDataTransferJobs();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const currentJob = findPortalDataTransferJob();
|
|
||||||
setPortalDataTransferJob(currentJob);
|
|
||||||
startPollingforUpdate(currentJob);
|
|
||||||
}, [dataTransferJobs]);
|
|
||||||
|
|
||||||
const isHierarchicalPartitionedContainer = (): boolean => collection.partitionKey?.kind === "MultiHash";
|
|
||||||
|
|
||||||
const getPartitionKeyValue = (): string => {
|
|
||||||
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
|
||||||
};
|
|
||||||
|
|
||||||
const partitionKeyName = "Partition key";
|
|
||||||
const partitionKeyValue = getPartitionKeyValue();
|
|
||||||
|
|
||||||
const textHeadingStyle = {
|
|
||||||
root: { fontWeight: FontWeights.semibold, fontSize: 16 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const textSubHeadingStyle = {
|
|
||||||
root: { fontWeight: FontWeights.semibold },
|
|
||||||
};
|
|
||||||
|
|
||||||
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => {
|
|
||||||
if (isCurrentJobInProgress(currentJob)) {
|
|
||||||
const jobName = currentJob?.properties?.jobName;
|
|
||||||
try {
|
|
||||||
pollDataTransferJob(
|
|
||||||
jobName,
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "ChangePartitionKey", `Failed to complete data transfer job ${jobName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
|
|
||||||
await cancelDataTransferJob(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
currentJob?.properties?.jobName,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => {
|
|
||||||
const jobStatus = currentJob?.properties?.status;
|
|
||||||
return (
|
|
||||||
jobStatus &&
|
|
||||||
jobStatus !== "Completed" &&
|
|
||||||
jobStatus !== "Cancelled" &&
|
|
||||||
jobStatus !== "Failed" &&
|
|
||||||
jobStatus !== "Faulted"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshDataTransferOperations = async () => {
|
|
||||||
await refreshDataTransferJobs(
|
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const findPortalDataTransferJob = (): DataTransferJobGetResults => {
|
|
||||||
return dataTransferJobs.find((feed: DataTransferJobGetResults) => {
|
|
||||||
const sourceSink: CosmosSqlDataTransferDataSourceSink = feed?.properties
|
|
||||||
?.source as CosmosSqlDataTransferDataSourceSink;
|
|
||||||
return sourceSink.databaseName === collection.databaseId && sourceSink.containerName === collection.id();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProgressDescription = (): string => {
|
|
||||||
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
|
||||||
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
|
||||||
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
|
|
||||||
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startPartitionkeyChangeWorkflow = () => {
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
"Change partition key",
|
|
||||||
<ChangePartitionKeyPane
|
|
||||||
sourceDatabase={database}
|
|
||||||
sourceCollection={collection}
|
|
||||||
explorer={explorer}
|
|
||||||
onClose={refreshDataTransferOperations}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPercentageComplete = () => {
|
|
||||||
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
|
||||||
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
|
||||||
const jobStatus = portalDataTransferJob?.properties?.status;
|
|
||||||
const isCancelled = jobStatus === "Cancelled";
|
|
||||||
const isCompleted = jobStatus === "Completed";
|
|
||||||
if (totalCount <= 0 && !isCompleted) {
|
|
||||||
return isCancelled ? 0 : null;
|
|
||||||
}
|
|
||||||
return isCompleted ? 1 : processedCount / totalCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
|
||||||
<Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>
|
|
||||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
|
||||||
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
|
||||||
<Text>{partitionKeyValue}</Text>
|
|
||||||
<Text>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<MessageBar messageBarType={MessageBarType.warning}>
|
|
||||||
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
|
|
||||||
source container for the entire duration of the partition key change process.
|
|
||||||
<Link
|
|
||||||
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
|
||||||
target="_blank"
|
|
||||||
underline
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</MessageBar>
|
|
||||||
<Text>
|
|
||||||
To change the partition key, a new destination container must be created or an existing destination container
|
|
||||||
selected. Data will then be copied to the destination container.
|
|
||||||
</Text>
|
|
||||||
<PrimaryButton
|
|
||||||
styles={{ root: { width: "fit-content" } }}
|
|
||||||
text="Change"
|
|
||||||
onClick={startPartitionkeyChangeWorkflow}
|
|
||||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
|
||||||
/>
|
|
||||||
{portalDataTransferJob && (
|
|
||||||
<Stack>
|
|
||||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
|
||||||
<Stack
|
|
||||||
horizontal
|
|
||||||
tokens={{ childrenGap: 20 }}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProgressIndicator
|
|
||||||
label={portalDataTransferJob?.properties?.jobName}
|
|
||||||
description={getProgressDescription()}
|
|
||||||
percentComplete={getPercentageComplete()}
|
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
width: "85%",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
></ProgressIndicator>
|
|
||||||
{isCurrentJobInProgress(portalDataTransferJob) && (
|
|
||||||
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const costElement = (): JSX.Element => {
|
const costElement = (): JSX.Element => {
|
||||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||||
return (
|
return (
|
||||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||||
{newThroughput && newThroughputCostElement()}
|
{newThroughput && newThroughputCostElement()}
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.0080
|
0.012
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -929,7 +929,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.19
|
0.29
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -941,7 +941,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
5.84
|
8.76
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.0080
|
0.012
|
||||||
/hr
|
/hr
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1366,7 +1366,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
0.19
|
0.29
|
||||||
/day
|
/day
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -1378,7 +1378,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
>
|
>
|
||||||
$
|
$
|
||||||
|
|
||||||
5.84
|
8.76
|
||||||
/mo
|
/mo
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export enum SettingsV2TabTypes {
|
|||||||
ConflictResolutionTab,
|
ConflictResolutionTab,
|
||||||
SubSettingsTab,
|
SubSettingsTab,
|
||||||
IndexingPolicyTab,
|
IndexingPolicyTab,
|
||||||
PartitionKeyTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsComponentDirtyResult {
|
export interface IsComponentDirtyResult {
|
||||||
@@ -147,8 +146,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
|||||||
return "Settings";
|
return "Settings";
|
||||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||||
return "Indexing Policy";
|
return "Indexing Policy";
|
||||||
case SettingsV2TabTypes.PartitionKeyTab:
|
|
||||||
return "Partition Keys";
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tab ${tab}`);
|
throw new Error(`Unknown tab ${tab}`);
|
||||||
}
|
}
|
||||||
@@ -202,49 +199,3 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => {
|
|||||||
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
|
||||||
// index transformation progress can be 0
|
// index transformation progress can be 0
|
||||||
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
||||||
|
|
||||||
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
|
|
||||||
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
|
|
||||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPartitionKeyTooltipText = (apiType: string): string => {
|
|
||||||
if (apiType === "Mongo") {
|
|
||||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
|
||||||
}
|
|
||||||
let tooltipText = `The ${getPartitionKeyName(
|
|
||||||
apiType,
|
|
||||||
true,
|
|
||||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
|
||||||
if (apiType === "SQL") {
|
|
||||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
|
||||||
}
|
|
||||||
return tooltipText;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
|
|
||||||
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
|
|
||||||
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
|
|
||||||
return subtext;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
|
|
||||||
switch (apiType) {
|
|
||||||
case "Mongo":
|
|
||||||
return "e.g., categoryId";
|
|
||||||
case "Gremlin":
|
|
||||||
return "e.g., /address";
|
|
||||||
case "SQL":
|
|
||||||
return `${
|
|
||||||
index === undefined
|
|
||||||
? "Required - first partition key e.g., /TenantId"
|
|
||||||
: index === 0
|
|
||||||
? "second partition key e.g., /UserId"
|
|
||||||
: "third partition key e.g., /SessionId"
|
|
||||||
}`;
|
|
||||||
default:
|
|
||||||
return "e.g., /address/zipCode";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -204,98 +204,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
shouldDiscardIndexingPolicy={false}
|
shouldDiscardIndexingPolicy={false}
|
||||||
/>
|
/>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
<PivotItem
|
|
||||||
headerText="Partition Keys"
|
|
||||||
itemKey="PartitionKeyTab"
|
|
||||||
key="PartitionKeyTab"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"marginTop": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PartitionKeyComponent
|
|
||||||
collection={
|
|
||||||
Object {
|
|
||||||
"analyticalStorageTtl": [Function],
|
|
||||||
"changeFeedPolicy": [Function],
|
|
||||||
"conflictResolutionPolicy": [Function],
|
|
||||||
"container": Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"_resetNotebookWorkspace": [Function],
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": Object {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"databaseId": "test",
|
|
||||||
"defaultTtl": [Function],
|
|
||||||
"geospatialConfig": [Function],
|
|
||||||
"getDatabase": [Function],
|
|
||||||
"id": [Function],
|
|
||||||
"indexingPolicy": [Function],
|
|
||||||
"offer": [Function],
|
|
||||||
"partitionKey": Object {
|
|
||||||
"kind": "hash",
|
|
||||||
"paths": Array [],
|
|
||||||
"version": 2,
|
|
||||||
},
|
|
||||||
"partitionKeyProperties": Array [
|
|
||||||
"partitionKey",
|
|
||||||
],
|
|
||||||
"readSettings": [Function],
|
|
||||||
"uniqueKeyPolicy": Object {},
|
|
||||||
"usageSizeInKB": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
explorer={
|
|
||||||
Explorer {
|
|
||||||
"_isInitializingNotebooks": false,
|
|
||||||
"_resetNotebookWorkspace": [Function],
|
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
|
||||||
"isTabsContentExpanded": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
|
||||||
"onRefreshResourcesClick": [Function],
|
|
||||||
"phoenixClient": PhoenixClient {
|
|
||||||
"armResourceId": undefined,
|
|
||||||
"retryOptions": Object {
|
|
||||||
"maxTimeout": 5000,
|
|
||||||
"minTimeout": 5000,
|
|
||||||
"retries": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"provideFeedbackEmail": [Function],
|
|
||||||
"queriesClient": QueriesClient {
|
|
||||||
"container": [Circular],
|
|
||||||
},
|
|
||||||
"refreshNotebookList": [Function],
|
|
||||||
"resourceTree": ResourceTreeAdapter {
|
|
||||||
"container": [Circular],
|
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PivotItem>
|
|
||||||
</StyledPivot>
|
</StyledPivot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
.throughputInputSpacing > :not(:last-child) {
|
.throughputInputSpacing > :not(:last-child) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
.capacitycalculator-link:focus {
|
.capacitycalculator-link:focus{
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
@@ -23,12 +23,12 @@ describe("ThroughputInput Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should switch mode properly", () => {
|
it("should switch mode properly", () => {
|
||||||
wrapper.find('[id="Manual-input"]').simulate("change");
|
wrapper.find('[aria-label="Manual database throughput"]').simulate("change");
|
||||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
|
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
|
||||||
"Container throughput (400 - unlimited RU/s)",
|
"Container throughput (400 - unlimited RU/s)",
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.find('[id="Autoscale-input"]').simulate("change");
|
wrapper.find('[aria-label="Autoscale database throughput"]').simulate("change");
|
||||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
<input
|
<input
|
||||||
id="Autoscale-input"
|
id="Autoscale-input"
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
aria-label={`${getThroughputLabelText()} Autoscale`}
|
aria-label="Autoscale database throughput"
|
||||||
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={`${getThroughputLabelText()} Manual`}
|
aria-label="Manual database throughput"
|
||||||
checked={!isAutoscaleSelected}
|
checked={!isAutoscaleSelected}
|
||||||
type="radio"
|
type="radio"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
@@ -224,10 +224,8 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
Estimate your required RU/s with{" "}
|
Estimate your required RU/s with{" "}
|
||||||
<Link
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="capacitycalculator-link"
|
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
style={{ outline: "none" }}
|
|
||||||
>
|
>
|
||||||
capacity calculator
|
capacity calculator
|
||||||
</Link>
|
</Link>
|
||||||
@@ -278,12 +276,6 @@ 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}
|
||||||
@@ -304,7 +296,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()}
|
||||||
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} Required RU/s`}
|
aria-label="Max request units per second"
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={throughputError}
|
errorMessage={throughputError}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-label="Container throughput (autoscale) Autoscale"
|
aria-label="Autoscale database throughput"
|
||||||
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="Container throughput (autoscale) Manual"
|
aria-label="Manual database throughput"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
checked={false}
|
checked={false}
|
||||||
className="throughputInputRadioBtn"
|
className="throughputInputRadioBtn"
|
||||||
@@ -733,24 +733,12 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
|
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="capacitycalculator-link"
|
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"outline": "none",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<LinkBase
|
<LinkBase
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="capacitycalculator-link"
|
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"outline": "none",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
styles={[Function]}
|
styles={[Function]}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
theme={
|
theme={
|
||||||
@@ -1029,14 +1017,9 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
aria-label="capacity calculator of azure cosmos db"
|
aria-label="capacity calculator of azure cosmos db"
|
||||||
className="ms-Link capacitycalculator-link root-117"
|
className="ms-Link root-117"
|
||||||
href="https://cosmos.azure.com/capacitycalculator/"
|
href="https://cosmos.azure.com/capacitycalculator/"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"outline": "none",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
capacity calculator
|
capacity calculator
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ 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, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
import { IGalleryItem } from "Juno/JunoClient";
|
||||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
import { requestDatabaseResourceTokens } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -265,43 +264,61 @@ 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 SEVEN_DAYS_IN_MS = 604800000;
|
const THREE_DAYS_IN_MS = 259200000;
|
||||||
|
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)) {
|
||||||
Logger.logInfo(
|
this.sendNPSMessage();
|
||||||
`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 {
|
||||||
// Show survey when an existing account is older than 7 days
|
// 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.
|
||||||
if (
|
if (
|
||||||
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", SEVEN_DAYS_IN_MS)
|
!isAccountNewerThanThresholdInMs(userContext.databaseAccount?.systemData?.createdAt || "", THREE_DAYS_IN_MS) &&
|
||||||
|
isAccountNewerThanNinetyDays
|
||||||
) {
|
) {
|
||||||
Logger.logInfo(
|
this.sendNPSMessage();
|
||||||
`Sending message to Portal to check if NPS Survey can be displayed for existing ${userContext.apiType} account older than 7 days`,
|
} else {
|
||||||
"Explorer/openNPSSurveyDialog",
|
// An existing account is greater than 90 days. For existing account show to random 33% of users in Data Explorer.
|
||||||
);
|
if (this.getRandomInt(100) < 33) {
|
||||||
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
this.sendNPSMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
private sendNPSMessage() {
|
||||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
sendMessage({ type: MessageTypes.DisplayNPSSurvey });
|
||||||
Logger.logInfo(
|
localStorage.setItem("lastSubmitted", Date.now().toString());
|
||||||
`CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`,
|
|
||||||
"Explorer/openCESCVAFeedbackBlade",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refreshDatabaseForResourceToken(): Promise<void> {
|
public async refreshDatabaseForResourceToken(): Promise<void> {
|
||||||
@@ -366,7 +383,9 @@ export default class Explorer {
|
|||||||
|
|
||||||
public onRefreshResourcesClick = (): void => {
|
public onRefreshResourcesClick = (): void => {
|
||||||
if (configContext.platform === Platform.Fabric) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
// Requesting the tokens will trigger a refresh of the databases
|
||||||
|
// TODO: Once the id is returned from Fabric, we can await this call and then refresh the databases here
|
||||||
|
requestDatabaseResourceTokens();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1370,18 +1389,9 @@ export default class Explorer {
|
|||||||
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
|
if (userContext.apiType !== "SQL" || !userContext.subscriptionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const copilotEnabledPromise = getCopilotEnabled();
|
const copilotEnabled = await getCopilotEnabled();
|
||||||
const copilotUserDBEnabledPromise = isCopilotFeatureRegistered(userContext.subscriptionId);
|
useQueryCopilot.getState().setCopilotEnabled(copilotEnabled);
|
||||||
const [copilotEnabled, copilotUserDBEnabled] = await Promise.all([
|
useQueryCopilot.getState().setCopilotUserDBEnabled(copilotEnabled);
|
||||||
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> {
|
||||||
|
|||||||
@@ -1163,12 +1163,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
)}"`,
|
)}"`,
|
||||||
).then(
|
).then(
|
||||||
(documents: DataModels.DocumentId[]) => {
|
(documents: DataModels.DocumentId[]) => {
|
||||||
$.each(documents, (index: number, doc: any) => {
|
$.each(
|
||||||
newIconsMap[doc["_graph_icon_property_value"]] = {
|
documents,
|
||||||
data: doc["icon"],
|
(index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => {
|
||||||
format: doc["format"],
|
newIconsMap[doc["_graph_icon_property_value"]] = {
|
||||||
};
|
data: doc["icon"],
|
||||||
});
|
format: doc["format"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Update graph configuration
|
// Update graph configuration
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -24,21 +24,16 @@ 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") {
|
||||||
@@ -47,7 +42,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" style={{ display: isHidden ? "none" : "initial" }}>
|
<div className="commandBarContainer">
|
||||||
<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)}
|
||||||
@@ -96,7 +91,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
? {
|
? {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
padding: "2px 8px 0px 8px",
|
padding: "0px 14px 0px 14px",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -106,7 +101,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="commandBarContainer" style={{ display: isHidden ? "none" : "initial" }}>
|
<div className="commandBarContainer">
|
||||||
<FluentCommandBar
|
<FluentCommandBar
|
||||||
ariaLabel="Use left and right arrow keys to navigate between commands"
|
ariaLabel="Use left and right arrow keys to navigate between commands"
|
||||||
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
items={uiFabricStaticButtons.concat(uiFabricTabsButtons)}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export function createStaticCommandBarButtons(
|
|||||||
buttons.push(newSqlQueryBtn);
|
buttons.push(newSqlQueryBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQuerySupported && selectedNodeState.findSelectedCollection() && configContext.platform !== Platform.Fabric) {
|
if (isQuerySupported && selectedNodeState.findSelectedCollection()) {
|
||||||
const openQueryBtn = createOpenQueryButton(container);
|
const openQueryBtn = createOpenQueryButton(container);
|
||||||
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton()];
|
||||||
buttons.push(openQueryBtn);
|
buttons.push(openQueryBtn);
|
||||||
@@ -196,22 +196,18 @@ export function createContextCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||||
const buttons: CommandButtonComponentProps[] =
|
const buttons: CommandButtonComponentProps[] = [
|
||||||
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
{
|
||||||
? []
|
iconSrc: SettingsIcon,
|
||||||
: [
|
iconAlt: "Settings",
|
||||||
{
|
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane />),
|
||||||
iconSrc: SettingsIcon,
|
commandButtonLabel: undefined,
|
||||||
iconAlt: "Settings",
|
ariaLabel: "Settings",
|
||||||
onCommandClick: () =>
|
tooltipText: "Settings",
|
||||||
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
|
hasPopup: true,
|
||||||
commandButtonLabel: undefined,
|
disabled: false,
|
||||||
ariaLabel: "Settings",
|
},
|
||||||
tooltipText: "Settings",
|
];
|
||||||
hasPopup: true,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const showOpenFullScreen =
|
const showOpenFullScreen =
|
||||||
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
|
||||||
@@ -240,7 +236,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
|||||||
const feedbackButtonOptions: CommandButtonComponentProps = {
|
const feedbackButtonOptions: CommandButtonComponentProps = {
|
||||||
iconSrc: FeedbackIcon,
|
iconSrc: FeedbackIcon,
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: () => container.openCESCVAFeedbackBlade(),
|
onCommandClick: () => container.provideFeedbackEmail(),
|
||||||
commandButtonLabel: undefined,
|
commandButtonLabel: undefined,
|
||||||
ariaLabel: label,
|
ariaLabel: label,
|
||||||
tooltipText: label,
|
tooltipText: label,
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ 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 =
|
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||||
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;
|
||||||
@@ -37,7 +34,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
return StyleConstants.GrayScale;
|
return StyleConstants.GrayScale;
|
||||||
}
|
}
|
||||||
return configContext.platform == Platform.Fabric ? StyleConstants.FabricToolbarIconColor : undefined;
|
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
return btns
|
return btns
|
||||||
@@ -96,12 +93,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
},
|
},
|
||||||
width: 16,
|
width: 16,
|
||||||
},
|
},
|
||||||
label: {
|
label: { fontSize: StyleConstants.mediumFontSize },
|
||||||
fontSize:
|
|
||||||
configContext.platform == Platform.Fabric
|
|
||||||
? StyleConstants.DefaultFontSize
|
|
||||||
: StyleConstants.mediumFontSize,
|
|
||||||
},
|
|
||||||
rootHovered: { backgroundColor: hoverColor },
|
rootHovered: { backgroundColor: hoverColor },
|
||||||
rootPressed: { backgroundColor: hoverColor },
|
rootPressed: { backgroundColor: hoverColor },
|
||||||
splitButtonMenuButtonExpanded: {
|
splitButtonMenuButtonExpanded: {
|
||||||
@@ -120,7 +112,6 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
splitButtonContainer: {
|
splitButtonContainer: {
|
||||||
marginLeft: 5,
|
marginLeft: 5,
|
||||||
marginRight: 5,
|
marginRight: 5,
|
||||||
height: buttonHeightPx,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
className: btn.className,
|
className: btn.className,
|
||||||
@@ -138,12 +129,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
|
|||||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||||
selectors: {
|
selectors: {
|
||||||
".ms-ContextualMenu-itemText": {
|
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||||
fontSize:
|
|
||||||
configContext.platform == Platform.Fabric
|
|
||||||
? StyleConstants.DefaultFontSize
|
|
||||||
: StyleConstants.mediumFontSize,
|
|
||||||
},
|
|
||||||
".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor },
|
".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor },
|
||||||
".ms-ContextualMenu-icon": { width: 16, height: 16 },
|
".ms-ContextualMenu-icon": { width: 16, height: 16 },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 explorer={explorer} />);
|
useSidePanel.getState().openSidePanel("Settings", <SettingsPane />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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";
|
||||||
@@ -13,7 +14,6 @@ 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,6 +63,9 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +75,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
|||||||
subscriptionQuotaId: userContext.quotaId,
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
throughput,
|
throughput,
|
||||||
|
flight: userContext.addCollectionFlight,
|
||||||
},
|
},
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,396 +0,0 @@
|
|||||||
import {
|
|
||||||
DefaultButton,
|
|
||||||
DirectionalHint,
|
|
||||||
Dropdown,
|
|
||||||
IDropdownOption,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Link,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TooltipHost,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import * as Constants from "Common/Constants";
|
|
||||||
import { handleError } from "Common/ErrorHandlingUtils";
|
|
||||||
import { createCollection } from "Common/dataAccess/createCollection";
|
|
||||||
import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers";
|
|
||||||
import * as DataModels from "Contracts/DataModels";
|
|
||||||
import * as ViewModels from "Contracts/ViewModels";
|
|
||||||
import {
|
|
||||||
getPartitionKeyName,
|
|
||||||
getPartitionKeyPlaceHolder,
|
|
||||||
getPartitionKeySubtext,
|
|
||||||
getPartitionKeyTooltipText,
|
|
||||||
} from "Explorer/Controls/Settings/SettingsUtils";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
|
||||||
import { useDatabases } from "Explorer/useDatabases";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import { getCollectionName } from "Utils/APITypeUtils";
|
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export interface ChangePartitionKeyPaneProps {
|
|
||||||
sourceDatabase: ViewModels.Database;
|
|
||||||
sourceCollection: ViewModels.Collection;
|
|
||||||
explorer: Explorer;
|
|
||||||
onClose: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
|
||||||
sourceDatabase,
|
|
||||||
sourceCollection,
|
|
||||||
explorer,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const [targetCollectionId, setTargetCollectionId] = React.useState<string>();
|
|
||||||
const [createNewContainer, setCreateNewContainer] = React.useState<boolean>(true);
|
|
||||||
const [formError, setFormError] = React.useState<string>();
|
|
||||||
const [isExecuting, setIsExecuting] = React.useState<boolean>(false);
|
|
||||||
const [subPartitionKeys, setSubPartitionKeys] = React.useState<string[]>([]);
|
|
||||||
const [partitionKey, setPartitionKey] = React.useState<string>();
|
|
||||||
|
|
||||||
const getCollectionOptions = (): IDropdownOption[] => {
|
|
||||||
return sourceDatabase
|
|
||||||
.collections()
|
|
||||||
.filter((collection) => collection.id !== sourceCollection.id)
|
|
||||||
.map((collection) => ({
|
|
||||||
key: collection.id(),
|
|
||||||
text: collection.id(),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!validateInputs()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsExecuting(true);
|
|
||||||
try {
|
|
||||||
createNewContainer && (await createContainer());
|
|
||||||
await createDataTransferJob();
|
|
||||||
await onClose();
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "ChangePartitionKey", "Failed to start data transfer job");
|
|
||||||
}
|
|
||||||
setIsExecuting(false);
|
|
||||||
useSidePanel.getState().closeSidePanel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateInputs = (): boolean => {
|
|
||||||
if (!createNewContainer && !targetCollectionId) {
|
|
||||||
setFormError("Choose an existing container");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDataTransferJob = async () => {
|
|
||||||
const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`;
|
|
||||||
const dataTransferParams: DataTransferParams = {
|
|
||||||
jobName,
|
|
||||||
apiType: userContext.apiType,
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
resourceGroupName: userContext.resourceGroup,
|
|
||||||
accountName: userContext.databaseAccount.name,
|
|
||||||
sourceDatabaseName: sourceDatabase.id(),
|
|
||||||
sourceCollectionName: sourceCollection.id(),
|
|
||||||
targetDatabaseName: sourceDatabase.id(),
|
|
||||||
targetCollectionName: targetCollectionId,
|
|
||||||
};
|
|
||||||
await initiateDataTransfer(dataTransferParams);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createContainer = async () => {
|
|
||||||
const partitionKeyString = partitionKey.trim();
|
|
||||||
const partitionKeyData: DataModels.PartitionKey = partitionKeyString
|
|
||||||
? {
|
|
||||||
paths: [partitionKeyString, ...(subPartitionKeys.length > 0 ? subPartitionKeys : [])],
|
|
||||||
kind: subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
|
||||||
version: 2,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const createCollectionParams: DataModels.CreateCollectionParams = {
|
|
||||||
createNewDatabase: false,
|
|
||||||
collectionId: targetCollectionId,
|
|
||||||
databaseId: sourceDatabase.id(),
|
|
||||||
databaseLevelThroughput: isSelectedDatabaseSharedThroughput(),
|
|
||||||
offerThroughput: sourceCollection.offer()?.manualThroughput,
|
|
||||||
autoPilotMaxThroughput: sourceCollection.offer()?.autoscaleMaxThroughput,
|
|
||||||
partitionKey: partitionKeyData,
|
|
||||||
};
|
|
||||||
await createCollection(createCollectionParams);
|
|
||||||
await explorer.refreshAllDatabases();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelectedDatabaseSharedThroughput = (): boolean => {
|
|
||||||
const selectedDatabase = useDatabases
|
|
||||||
.getState()
|
|
||||||
.databases?.find((database) => database.id() === sourceDatabase.id());
|
|
||||||
return !!selectedDatabase?.offer();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK">
|
|
||||||
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
|
|
||||||
<Text variant="small">
|
|
||||||
When changing a container’s partition key, you will need to create a destination container with the correct
|
|
||||||
partition key. You may also select an existing destination container.
|
|
||||||
<Link
|
|
||||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
|
|
||||||
target="_blank"
|
|
||||||
underline
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
<Stack>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Database id
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
|
||||||
true,
|
|
||||||
).toLocaleLowerCase()}.`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
iconName="Info"
|
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
|
||||||
true,
|
|
||||||
).toLocaleLowerCase()}.`}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
<Dropdown
|
|
||||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
|
||||||
style={{ width: 300, fontSize: 12 }}
|
|
||||||
options={[]}
|
|
||||||
placeholder={sourceDatabase.id()}
|
|
||||||
responsiveMode={999}
|
|
||||||
disabled={true}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Stack className="panelGroupSpacing" horizontal verticalAlign="center">
|
|
||||||
<div role="radiogroup">
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={createNewContainer}
|
|
||||||
aria-label="Create new container"
|
|
||||||
aria-checked={createNewContainer}
|
|
||||||
name="containerType"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="containerCreateNew"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={() => setCreateNewContainer(true)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">New container</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={!createNewContainer}
|
|
||||||
aria-label="Use existing container"
|
|
||||||
aria-checked={!createNewContainer}
|
|
||||||
name="containerType"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={() => setCreateNewContainer(false)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Existing container</span>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
{createNewContainer ? (
|
|
||||||
<Stack>
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
{`${getCollectionName()} id`}
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
role="button"
|
|
||||||
iconName="Info"
|
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
<input
|
|
||||||
name="collectionId"
|
|
||||||
id="collectionId"
|
|
||||||
type="text"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
|
||||||
placeholder={`e.g., ${getCollectionName()}1`}
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
|
||||||
value={targetCollectionId}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
{getPartitionKeyName(userContext.apiType)}
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={getPartitionKeyTooltipText(userContext.apiType)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
iconName="Info"
|
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={getPartitionKeyTooltipText(userContext.apiType)}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Text variant="small" aria-label="pkDescription">
|
|
||||||
{getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="addCollection-partitionKeyValue"
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
placeholder={getPartitionKeyPlaceHolder(userContext.apiType)}
|
|
||||||
aria-label={getPartitionKeyName(userContext.apiType)}
|
|
||||||
pattern={".*"}
|
|
||||||
title={""}
|
|
||||||
value={partitionKey}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!partitionKey && !event.target.value.startsWith("/")) {
|
|
||||||
setPartitionKey("/" + event.target.value);
|
|
||||||
} else {
|
|
||||||
setPartitionKey(event.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{subPartitionKeys.map((subPartitionKey: string, index: number) => {
|
|
||||||
return (
|
|
||||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "20px",
|
|
||||||
border: "solid",
|
|
||||||
borderWidth: "0px 0px 1px 1px",
|
|
||||||
marginRight: "5px",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="addCollection-partitionKeyValue"
|
|
||||||
key={`addCollection-partitionKeyValue_${index}`}
|
|
||||||
aria-required
|
|
||||||
required
|
|
||||||
size={40}
|
|
||||||
tabIndex={index > 0 ? 1 : 0}
|
|
||||||
className="panelTextField"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder={getPartitionKeyPlaceHolder(userContext.apiType, index)}
|
|
||||||
aria-label={getPartitionKeyName(userContext.apiType)}
|
|
||||||
pattern={".*"}
|
|
||||||
title={""}
|
|
||||||
value={subPartitionKey}
|
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const keys = [...subPartitionKeys];
|
|
||||||
if (!keys[index] && !event.target.value.startsWith("/")) {
|
|
||||||
keys[index] = "/" + event.target.value.trim();
|
|
||||||
setSubPartitionKeys(keys);
|
|
||||||
} else {
|
|
||||||
keys[index] = event.target.value.trim();
|
|
||||||
setSubPartitionKeys(keys);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "Delete" }}
|
|
||||||
style={{ height: 27 }}
|
|
||||||
onClick={() => {
|
|
||||||
const keys = subPartitionKeys.filter((uniqueKey, j) => index !== j);
|
|
||||||
setSubPartitionKeys(keys);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<DefaultButton
|
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
|
||||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
|
||||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
|
||||||
>
|
|
||||||
Add hierarchical partition key
|
|
||||||
</DefaultButton>
|
|
||||||
{subPartitionKeys.length > 0 && (
|
|
||||||
<Text variant="small">
|
|
||||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
|
||||||
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
|
|
||||||
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
|
||||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
) : (
|
|
||||||
<Stack>
|
|
||||||
<Stack horizontal>
|
|
||||||
<span className="mandatoryStar">* </span>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
{`${getCollectionName()}`}
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
role="button"
|
|
||||||
iconName="Info"
|
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
|
||||||
style={{ width: 300, fontSize: 12 }}
|
|
||||||
placeholder="Choose an existing container"
|
|
||||||
options={getCollectionOptions()}
|
|
||||||
onChange={(event: React.FormEvent<HTMLDivElement>, collection: IDropdownOption) => {
|
|
||||||
setTargetCollectionId(collection.key as string);
|
|
||||||
setFormError("");
|
|
||||||
}}
|
|
||||||
defaultSelectedKey={targetCollectionId}
|
|
||||||
responsiveMode={999}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</RightPaneForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -29,7 +29,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
omponentDidMount(): 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 },
|
content: { padding: 0, height: "100%" },
|
||||||
|
scrollableContent: { height: "100%" },
|
||||||
header: { padding: "0 0 8px 34px" },
|
header: { padding: "0 0 8px 34px" },
|
||||||
commands: { marginTop: 8, paddingTop: 0 },
|
commands: { marginTop: 8 },
|
||||||
}}
|
}}
|
||||||
style={{ height: this.state.height }}
|
style={{ height: this.state.height }}
|
||||||
>
|
>
|
||||||
@@ -76,7 +76,44 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isFocusable(element: HTMLElement) {
|
||||||
|
return (
|
||||||
|
element.tabIndex >= 0 ||
|
||||||
|
(element instanceof HTMLAnchorElement && element.href) ||
|
||||||
|
(element instanceof HTMLAreaElement && element.href) ||
|
||||||
|
(element instanceof HTMLInputElement && !element.disabled) ||
|
||||||
|
(element instanceof HTMLSelectElement && !element.disabled) ||
|
||||||
|
(element instanceof HTMLTextAreaElement && !element.disabled) ||
|
||||||
|
(element instanceof HTMLButtonElement && !element.disabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
private findFocusableParent = (element: HTMLElement) => {
|
||||||
|
while (element) {
|
||||||
|
if (this.isFocusable(element)) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
element = element.parentNode as HTMLElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
private onDissmiss = (ev?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
|
private onDissmiss = (ev?: KeyboardEvent | React.SyntheticEvent<HTMLElement>): void => {
|
||||||
|
const elementIdToFocus = sessionStorage.getItem("focusedElementId") || null;
|
||||||
|
if (elementIdToFocus) {
|
||||||
|
const targetElement = document.getElementById(elementIdToFocus);
|
||||||
|
|
||||||
|
if (targetElement) {
|
||||||
|
const focusableParent = this.findFocusableParent(targetElement);
|
||||||
|
if (focusableParent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (focusableParent) {
|
||||||
|
focusableParent.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
sessionStorage.removeItem("focusedElementId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
if (ev && (ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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 explorer={null} />);
|
const wrapper = shallow(<SettingsPane />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ describe("Settings Pane", () => {
|
|||||||
},
|
},
|
||||||
} as DatabaseAccount,
|
} as DatabaseAccount,
|
||||||
});
|
});
|
||||||
const wrapper = shallow(<SettingsPane explorer={null} />);
|
const wrapper = shallow(<SettingsPane />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,38 +11,23 @@ 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 {
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
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<{ explorer: Explorer }> = ({
|
export const SettingsPane: FunctionComponent = () => {
|
||||||
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),
|
||||||
);
|
);
|
||||||
@@ -93,17 +78,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
? 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 shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled;
|
const handlerOnSubmit = () => {
|
||||||
const handlerOnSubmit = async () => {
|
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
LocalStorageUtility.setEntryNumber(
|
LocalStorageUtility.setEntryNumber(
|
||||||
@@ -111,7 +92,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage,
|
||||||
);
|
);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled);
|
|
||||||
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts);
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval);
|
LocalStorageUtility.setEntryNumber(StorageKey.RetryInterval, retryInterval);
|
||||||
@@ -120,7 +100,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
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(
|
||||||
@@ -129,10 +108,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ruThresholdEnabled) {
|
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.RUThreshold, ruThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queryTimeoutEnabled) {
|
if (queryTimeoutEnabled) {
|
||||||
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
|
LocalStorageUtility.setEntryNumber(StorageKey.QueryTimeout, queryTimeout);
|
||||||
LocalStorageUtility.setEntryBoolean(
|
LocalStorageUtility.setEntryBoolean(
|
||||||
@@ -164,7 +139,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
logConsoleInfo(
|
logConsoleInfo(
|
||||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
||||||
);
|
);
|
||||||
refreshExplorer && (await explorer.refreshExplorer());
|
|
||||||
closeSidePanel();
|
closeSidePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -208,17 +182,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
setPageOption(option.key);
|
setPageOption(option.key);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnRUThresholdToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
|
||||||
setRUThresholdEnabled(checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnRUThresholdSpinButtonChange = (ev: React.MouseEvent<HTMLElement>, newValue?: string): void => {
|
|
||||||
const ruThreshold = Number(newValue);
|
|
||||||
if (!isNaN(ruThreshold)) {
|
|
||||||
setRUThreshold(ruThreshold);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
const handleOnQueryTimeoutToggleChange = (ev: React.MouseEvent<HTMLElement>, checked?: boolean): void => {
|
||||||
setQueryTimeoutEnabled(checked);
|
setQueryTimeoutEnabled(checked);
|
||||||
};
|
};
|
||||||
@@ -255,12 +218,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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",
|
||||||
@@ -283,7 +240,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleStyles: IToggleStyles = {
|
const queryTimeoutToggleStyles: IToggleStyles = {
|
||||||
label: {
|
label: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
@@ -296,7 +253,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
text: {},
|
text: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const spinButtonStyles: ISpinButtonStyles = {
|
const queryTimeoutSpinButtonStyles: ISpinButtonStyles = {
|
||||||
label: {
|
label: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
@@ -362,83 +319,48 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<>
|
<div className="settingsSection">
|
||||||
<div className="settingsSection">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionPart">
|
<div>
|
||||||
<div>
|
<legend id="queryTimeoutLabel" className="settingsSectionLabel legendLabel">
|
||||||
<legend id="ruThresholdLabel" className="settingsSectionLabel legendLabel">
|
Query Timeout
|
||||||
RU Threshold
|
</legend>
|
||||||
</legend>
|
<InfoTooltip>
|
||||||
<InfoTooltip>If a query exceeds a configured RU threshold, the query will be aborted.</InfoTooltip>
|
When a query reaches a specified time limit, a popup with an option to cancel the query will show
|
||||||
</div>
|
unless automatic cancellation has been enabled
|
||||||
|
</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
|
||||||
styles={toggleStyles}
|
label="Automatically cancel query after timeout"
|
||||||
label="Enable RU threshold"
|
styles={queryTimeoutToggleStyles}
|
||||||
onChange={handleOnRUThresholdToggleChange}
|
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
||||||
defaultChecked={ruThresholdEnabled}
|
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
||||||
/>
|
/>
|
||||||
</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 className="settingsSection">
|
</div>
|
||||||
<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">
|
||||||
@@ -463,7 +385,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||||
styles={spinButtonStyles}
|
styles={queryTimeoutSpinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
@@ -485,7 +407,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||||
styles={spinButtonStyles}
|
styles={queryTimeoutSpinButtonStyles}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
<legend id="queryRetryAttemptsLabel" className="settingsSectionLabel legendLabel">
|
||||||
@@ -507,12 +429,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
onIncrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)}
|
||||||
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)}
|
||||||
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)}
|
||||||
styles={spinButtonStyles}
|
styles={queryTimeoutSpinButtonStyles}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionLabel">
|
<div className="settingsSectionLabel">
|
||||||
Enable container pagination
|
Enable container pagination
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
@@ -532,7 +454,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</div>
|
</div>
|
||||||
{shouldShowCrossPartitionOption && (
|
{shouldShowCrossPartitionOption && (
|
||||||
<div className="settingsSection">
|
<div className="settingsSection">
|
||||||
<div className="settingsSectionPart settingsSectionInlineCheckbox">
|
<div className="settingsSectionPart">
|
||||||
<div className="settingsSectionLabel">
|
<div className="settingsSectionLabel">
|
||||||
Enable cross-partition query
|
Enable cross-partition query
|
||||||
<InfoTooltip>
|
<InfoTooltip>
|
||||||
@@ -623,30 +545,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</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>
|
||||||
|
|||||||
@@ -97,74 +97,6 @@ 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"
|
||||||
>
|
>
|
||||||
@@ -342,7 +274,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart settingsSectionInlineCheckbox"
|
className="settingsSectionPart"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionLabel"
|
className="settingsSectionLabel"
|
||||||
@@ -371,7 +303,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
|||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart settingsSectionInlineCheckbox"
|
className="settingsSectionPart"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionLabel"
|
className="settingsSectionLabel"
|
||||||
@@ -589,7 +521,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
|||||||
className="settingsSection"
|
className="settingsSection"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionPart settingsSectionInlineCheckbox"
|
className="settingsSectionPart"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="settingsSectionLabel"
|
className="settingsSectionLabel"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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";
|
||||||
@@ -98,19 +97,9 @@ 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, value } = entities[i];
|
const { property, type } = entities[i];
|
||||||
if ((property === "PartitionKey" && value === "") || (property === "RowKey" && value === "")) {
|
if (property === "" || property === undefined) {
|
||||||
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
|
setFormError(`Property name cannot be empty. Please enter a property name`);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +107,6 @@ 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);
|
||||||
@@ -140,13 +127,6 @@ 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);
|
||||||
@@ -202,14 +182,9 @@ 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().trim();
|
cloneEntities[indexOfInput].property = value.toString();
|
||||||
} else if (key === "time") {
|
} else if (key === "time") {
|
||||||
cloneEntities[indexOfInput].entityTimeValue = value.toString();
|
cloneEntities[indexOfInput].entityTimeValue = value.toString();
|
||||||
} else if (
|
|
||||||
cloneEntities[indexOfInput].property === "PartitionKey" ||
|
|
||||||
cloneEntities[indexOfInput].property === "RowKey"
|
|
||||||
) {
|
|
||||||
cloneEntities[indexOfInput].value = value.toString().trim();
|
|
||||||
} else {
|
} else {
|
||||||
cloneEntities[indexOfInput].value = value.toString();
|
cloneEntities[indexOfInput].value = value.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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";
|
||||||
@@ -191,7 +190,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, value } = entities[i];
|
const { property, type } = 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;
|
||||||
@@ -201,17 +200,6 @@ 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);
|
||||||
@@ -371,7 +359,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}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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]}
|
||||||
@@ -19,9 +18,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 {
|
||||||
@@ -30,6 +29,9 @@ 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}
|
||||||
@@ -46,7 +48,6 @@ 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]}
|
||||||
@@ -59,9 +60,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 {
|
||||||
@@ -70,6 +71,9 @@ 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}
|
||||||
|
|||||||
@@ -50,9 +50,7 @@ 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" as={"h1"}>
|
<Text className="title bold">Welcome to Microsoft Copilot for Azure in Cosmos DB</Text>
|
||||||
Welcome to Microsoft Copilot for Azure in Cosmos DB (preview)
|
|
||||||
</Text>
|
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
<Stack.Item align="center" className="text">
|
<Stack.Item align="center" className="text">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
|
|||||||
@@ -67,10 +67,9 @@ 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 (preview)
|
Welcome to Microsoft Copilot for Azure in Cosmos DB
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem
|
<StackItem
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const CopyPopup = ({
|
|||||||
|
|
||||||
return showCopyPopup ? (
|
return showCopyPopup ? (
|
||||||
<Stack
|
<Stack
|
||||||
role="status"
|
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
width: 345,
|
width: 345,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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",
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ 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,
|
||||||
@@ -66,7 +65,6 @@ 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 }),
|
||||||
@@ -105,7 +103,6 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
queryResults: undefined,
|
queryResults: undefined,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
isSamplePromptsOpen: false,
|
isSamplePromptsOpen: false,
|
||||||
showPromptTeachingBubble: true,
|
|
||||||
showDeletePopup: false,
|
showDeletePopup: false,
|
||||||
showFeedbackBar: false,
|
showFeedbackBar: false,
|
||||||
showCopyPopup: false,
|
showCopyPopup: false,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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";
|
||||||
@@ -70,7 +71,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
databaseId,
|
databaseId,
|
||||||
containerId,
|
containerId,
|
||||||
}: QueryCopilotPromptProps): JSX.Element => {
|
}: QueryCopilotPromptProps): JSX.Element => {
|
||||||
const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState<boolean>(false);
|
const [copilotTeachingBubbleVisible, { toggle: toggleCopilotTeachingBubbleVisible }] = useBoolean(false);
|
||||||
const inputEdited = useRef(false);
|
const inputEdited = useRef(false);
|
||||||
const {
|
const {
|
||||||
openFeedbackModal,
|
openFeedbackModal,
|
||||||
@@ -93,8 +94,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
setIsSamplePromptsOpen,
|
setIsSamplePromptsOpen,
|
||||||
showSamplePrompts,
|
showSamplePrompts,
|
||||||
setShowSamplePrompts,
|
setShowSamplePrompts,
|
||||||
showPromptTeachingBubble,
|
|
||||||
setShowPromptTeachingBubble,
|
|
||||||
showDeletePopup,
|
showDeletePopup,
|
||||||
setShowDeletePopup,
|
setShowDeletePopup,
|
||||||
showFeedbackBar,
|
showFeedbackBar,
|
||||||
@@ -273,23 +272,16 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showTeachingBubble = (): void => {
|
const showTeachingBubble = (): void => {
|
||||||
if (showPromptTeachingBubble && !inputEdited.current) {
|
if (!inputEdited.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!inputEdited.current && !isWelcomModalVisible()) {
|
if (!inputEdited.current && !isWelcomModalVisible()) {
|
||||||
setCopilotTeachingBubbleVisible(true);
|
toggleCopilotTeachingBubbleVisible();
|
||||||
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";
|
||||||
};
|
};
|
||||||
@@ -311,29 +303,15 @@ 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
|
<Stack className="copilot-prompt-pane" styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}>
|
||||||
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 }} alt="Copilot" role="none" />
|
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} />
|
||||||
<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 } }}
|
||||||
@@ -348,7 +326,6 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
ariaLabel="Close"
|
ariaLabel="Close"
|
||||||
title="Close copilot"
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
@@ -371,15 +348,14 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
disabled={isGeneratingQuery}
|
disabled={isGeneratingQuery}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||||
aria-labelledby="copilot-textfield-label"
|
|
||||||
/>
|
/>
|
||||||
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
|
{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(false)}
|
onDismiss={toggleCopilotTeachingBubbleVisible}
|
||||||
hasSmallHeadline={true}
|
hasSmallHeadline={true}
|
||||||
headline="Write a prompt"
|
headline="Write a prompt"
|
||||||
>
|
>
|
||||||
@@ -387,7 +363,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
<Link
|
<Link
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowSamplePrompts(true);
|
setShowSamplePrompts(true);
|
||||||
toggleCopilotTeachingBubbleVisible(false);
|
toggleCopilotTeachingBubbleVisible();
|
||||||
}}
|
}}
|
||||||
style={{ color: "white", fontWeight: 600 }}
|
style={{ color: "white", fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
@@ -401,11 +377,8 @@ 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"
|
|
||||||
/>
|
/>
|
||||||
<div role="alert" aria-label={getAriaLabel()}>
|
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
|
||||||
</div>
|
|
||||||
{showSamplePrompts && (
|
{showSamplePrompts && (
|
||||||
<Callout
|
<Callout
|
||||||
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
|
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
|
||||||
@@ -511,7 +484,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's accurate and appropriate before using it.{" "}
|
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||||
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072c9" }}>
|
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank">
|
||||||
Read preview terms
|
Read preview terms
|
||||||
</Link>
|
</Link>
|
||||||
{showErrorMessageBar && (
|
{showErrorMessageBar && (
|
||||||
@@ -543,7 +516,6 @@ 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={() => {
|
||||||
@@ -579,18 +551,10 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -598,24 +562,16 @@ 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}
|
||||||
|
|||||||
@@ -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 { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { 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 as Partial<QueryCopilotState>),
|
onCommandClick: () => OnExecuteQueryClick(useQueryCopilot),
|
||||||
commandButtonLabel: executeQueryBtnLabel,
|
commandButtonLabel: executeQueryBtnLabel,
|
||||||
ariaLabel: executeQueryBtnLabel,
|
ariaLabel: executeQueryBtnLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ 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 {
|
import { ContainerConnectionInfo, CopilotEnabledConfiguration, IProvisionData } from "Contracts/DataModels";
|
||||||
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";
|
||||||
@@ -57,28 +52,6 @@ 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();
|
||||||
|
|||||||
@@ -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 { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { 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,11 +12,7 @@ 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(
|
QueryDocumentsPerPage(firstItemIndex, useQueryCopilot.getState().queryIterator, useQueryCopilot)
|
||||||
firstItemIndex,
|
|
||||||
useQueryCopilot.getState().queryIterator,
|
|
||||||
useQueryCopilot as Partial<QueryCopilotState>,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ exports[`Footer snapshot test should not pass if no text 1`] = `
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
disabled={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
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={null}
|
disabled={false}
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Send",
|
"iconName": "Send",
|
||||||
|
|||||||
@@ -148,25 +148,23 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||||
{useQueryCopilot.getState().copilotEnabled && (
|
<SplashScreenButton
|
||||||
<SplashScreenButton
|
imgSrc={CopilotIcon}
|
||||||
imgSrc={CopilotIcon}
|
title={"Query faster with Copilot"}
|
||||||
title={"Query faster with Copilot"}
|
description={
|
||||||
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!"
|
||||||
"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);
|
||||||
}
|
}
|
||||||
onClick={() => {
|
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
||||||
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"}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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";
|
||||||
@@ -96,7 +94,7 @@ function createDataTable(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTable.Config>{
|
tableEntityListViewModel.table = DataTableBuilder.createDataTable($dataTable, <DataTables.Settings>{
|
||||||
// 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,
|
||||||
@@ -118,7 +116,7 @@ function createDataTable(
|
|||||||
sPrevious: "<",
|
sPrevious: "<",
|
||||||
sLast: ">>",
|
sLast: ">>",
|
||||||
},
|
},
|
||||||
sProcessing: `<img style="width: 28px; height: 6px; " src="${loadingIndicator3Squares}">`,
|
sProcessing: '<img style="width: 28px; height: 6px; " src="images/LoadingIndicator_3Squares.gif">',
|
||||||
oAria: {
|
oAria: {
|
||||||
sSortAscending: "",
|
sSortAscending: "",
|
||||||
sSortDescending: "",
|
sSortDescending: "",
|
||||||
@@ -347,7 +345,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<Element> = $(document.activeElement);
|
var $activeElement: JQuery = $(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;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as DataTable from "datatables.net-dt";
|
|
||||||
import * as Utilities from "../Utilities";
|
import * as Utilities from "../Utilities";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,7 +8,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): DataTable.Api<HTMLElement> {
|
export function createDataTable($dataTableElem: JQuery, settings: any): DataTables.DataTable {
|
||||||
return $dataTableElem.DataTable(applyDefaultRendering(settings));
|
return $dataTableElem.DataTable(applyDefaultRendering(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,14 +18,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: DataTable.Config): any {
|
function applyDefaultRendering(settings: any): DataTables.SettingsLegacy {
|
||||||
var tableColumns: any[] = null;
|
var tableColumns: DataTables.ColumnLegacy[] = null;
|
||||||
|
|
||||||
if (settings.columns) {
|
if (settings.aoColumns) {
|
||||||
tableColumns = settings.columns;
|
tableColumns = settings.aoColumns;
|
||||||
} else if (settings.columnDefs) {
|
} else if (settings.aoColumnDefs) {
|
||||||
// for tables we use aoColumnDefs instead of aoColumns
|
// for tables we use aoColumnDefs instead of aoColumns
|
||||||
tableColumns = settings.columnDefs;
|
tableColumns = settings.aoColumnDefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// either the settings had no columns defined, or they were called
|
// either the settings had no columns defined, or they were called
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
|
||||||
import * as Constants from "../Constants";
|
|
||||||
import * as Entities from "../Entities";
|
|
||||||
import * as Utilities from "../Utilities";
|
|
||||||
import * as DataTableOperations from "./DataTableOperations";
|
import * as DataTableOperations from "./DataTableOperations";
|
||||||
|
import * as Constants from "../Constants";
|
||||||
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 TableEntityListViewModel from "./TableEntityListViewModel";
|
|||||||
export default class DataTableOperationManager {
|
export default class DataTableOperationManager {
|
||||||
private _tableEntityListViewModel: TableEntityListViewModel;
|
private _tableEntityListViewModel: TableEntityListViewModel;
|
||||||
private _tableCommands: TableCommands;
|
private _tableCommands: TableCommands;
|
||||||
private dataTable: JQuery<Element>;
|
private dataTable: JQuery;
|
||||||
|
|
||||||
constructor(table: JQuery<Element>, viewModel: TableEntityListViewModel, tableCommands: TableCommands) {
|
constructor(table: JQuery, 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<Element> = $(event.currentTarget);
|
var elem: JQuery = $(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<Element> = $(Constants.htmlSelectors.dataTableAllRowsSelector);
|
var dataTableRows: JQuery = $(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<Element> = dataTableRows.eq(safeIndex);
|
var selectedRowElement: JQuery = 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<Element>): Entities.ITableEntityIdentity {
|
private getEntityIdentity($elem: JQuery): Entities.ITableEntityIdentity {
|
||||||
return {
|
return {
|
||||||
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
RowKey: $elem.attr(Constants.htmlAttributeNames.dataTableRowKeyAttr),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLastSelectedItem($elem: JQuery<Element>, isShiftSelect: boolean) {
|
private updateLastSelectedItem($elem: JQuery, 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<Element>) {
|
private applySingleSelection($elem: JQuery) {
|
||||||
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<Element>): void {
|
private applyCtrlSelection($elem: JQuery): 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<Element>): void {
|
private applyShiftSelection($elem: JQuery): 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<Element>) {
|
private applyContextMenuSelection($elem: JQuery) {
|
||||||
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
var entityIdentity: Entities.ITableEntityIdentity = this.getEntityIdentity($elem);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -14,7 +13,7 @@ export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
|
|||||||
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
|
return QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector + selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRowVisible(dataTableScrollBodyQuery: JQuery<Element>, element: Element): boolean {
|
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
|
||||||
let isVisible = false;
|
let isVisible = false;
|
||||||
|
|
||||||
if (dataTableScrollBodyQuery.length && element) {
|
if (dataTableScrollBodyQuery.length && element) {
|
||||||
@@ -27,18 +26,16 @@ export function isRowVisible(dataTableScrollBodyQuery: JQuery<Element>, element:
|
|||||||
return isVisible;
|
return isVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scrollToRowIfNeeded(dataTableRows: JQuery<Element>, currentIndex: number, isScrollUp: boolean): void {
|
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
|
||||||
if (dataTableRows.length) {
|
if (dataTableRows.length) {
|
||||||
const dataTableScrollBodyQuery: JQuery<Element> = $(
|
const dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
|
||||||
QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector,
|
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
|
||||||
),
|
|
||||||
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<Element> = $(selectedRowElement),
|
const selectedRowQuery: JQuery = $(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;
|
||||||
@@ -57,8 +54,8 @@ export function scrollToRowIfNeeded(dataTableRows: JQuery<Element>, currentIndex
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function scrollToTopIfNeeded(): void {
|
export function scrollToTopIfNeeded(): void {
|
||||||
const $dataTableRows: JQuery<Element> = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
|
const $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
|
||||||
$dataTableScrollBody: JQuery<Element> = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
|
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
|
||||||
|
|
||||||
if ($dataTableRows.length && $dataTableScrollBody.length) {
|
if ($dataTableRows.length && $dataTableScrollBody.length) {
|
||||||
$dataTableScrollBody.scrollTop(0);
|
$dataTableScrollBody.scrollTop(0);
|
||||||
@@ -74,7 +71,7 @@ export function setPaginationButtonEventHandlers(): void {
|
|||||||
.attr("role", "button");
|
.attr("role", "button");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterColumns(table: DataTables.Api<HTMLElement>, settings: boolean[]): void {
|
export function filterColumns(table: DataTables.DataTable, 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);
|
||||||
@@ -87,7 +84,7 @@ export function filterColumns(table: DataTables.Api<HTMLElement>, settings: bool
|
|||||||
* 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.Api<HTMLElement>,
|
table: DataTables.DataTable,
|
||||||
targetOrder: number[],
|
targetOrder: number[],
|
||||||
currentOrder?: number[],
|
currentOrder?: number[],
|
||||||
//eslint-disable-next-line
|
//eslint-disable-next-line
|
||||||
@@ -111,9 +108,7 @@ export function reorderColumns(
|
|||||||
? calculateTransformationOrder(currentOrder, targetOrder)
|
? calculateTransformationOrder(currentOrder, targetOrder)
|
||||||
: targetOrder;
|
: targetOrder;
|
||||||
try {
|
try {
|
||||||
// TODO: This possibly does not work with the new version of datatables.
|
$.fn.dataTable.ColReorder(table).fnOrder(transformationOrder);
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
@@ -121,9 +116,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.
|
||||||
@@ -138,10 +133,8 @@ 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.Api<HTMLElement>): number[] {
|
export function getCurrentOrder(table: DataTables.DataTable): number[] {
|
||||||
// TODO: This possibly does not work with the new version of datatables.
|
return $.fn.dataTable.ColReorder(table).fnOrder();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
return ($.fn.dataTable as any).ColReorder(table).fnOrder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,8 +178,8 @@ export function calculateTransformationOrder(currentOrder: number[], targetOrder
|
|||||||
return transformationOrder;
|
return transformationOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDataTableHeaders(table: DataTables.Api<HTMLElement>): string[] {
|
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
|
||||||
const columns = table.columns();
|
const columns: DataTables.ColumnsMethods = 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
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
|
|
||||||
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
|
||||||
import * as DataTables from "datatables.net";
|
|
||||||
import * as CommonConstants from "../../../Common/Constants";
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import CacheBase from "./CacheBase";
|
||||||
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
import * as CommonConstants from "../../../Common/Constants";
|
||||||
import * as Constants from "../Constants";
|
import * as Constants from "../Constants";
|
||||||
import * as Entities from "../Entities";
|
import * as Entities from "../Entities";
|
||||||
import CacheBase from "./CacheBase";
|
import QueryTablesTab from "../../Tabs/QueryTablesTab";
|
||||||
|
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.
|
||||||
@@ -28,7 +27,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.Api<HTMLElement>;
|
public table: DataTables.DataTable;
|
||||||
|
|
||||||
// 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;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -57,7 +56,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"
|
||||||
@@ -132,7 +131,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.Api<Element> {
|
public reloadTable(useSetting: boolean = true, resetHeaders: boolean = true): DataTables.DataTable {
|
||||||
this.clearCache();
|
this.clearCache();
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
this.isCancelled = false;
|
this.isCancelled = false;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -644,7 +643,7 @@ export default class QueryBuilderViewModel {
|
|||||||
return groupViewModels;
|
return groupViewModels;
|
||||||
};
|
};
|
||||||
|
|
||||||
public runQuery = (): DataTable.Api<Element> => {
|
public runQuery = (): DataTables.DataTable => {
|
||||||
return this._queryViewModel.runQuery();
|
return this._queryViewModel.runQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
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 { userContext } from "../../../UserContext";
|
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
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";
|
||||||
@@ -159,7 +158,7 @@ export default class QueryViewModel {
|
|||||||
notify: "always",
|
notify: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
public runQuery = (): DataTables.Api<Element> => {
|
public runQuery = (): DataTables.DataTable => {
|
||||||
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, "'");
|
||||||
@@ -177,7 +176,7 @@ export default class QueryViewModel {
|
|||||||
return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false);
|
return this._tableEntityListViewModel.reloadTable(/*useSetting*/ false, /*resetHeaders*/ false);
|
||||||
};
|
};
|
||||||
|
|
||||||
public clearQuery = (): DataTables.Api<Element> => {
|
public clearQuery = (): DataTables.DataTable => {
|
||||||
this.queryText();
|
this.queryText();
|
||||||
this.topValue();
|
this.topValue();
|
||||||
this.selectText();
|
this.selectText();
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
query,
|
query,
|
||||||
paginationToken,
|
paginationToken,
|
||||||
},
|
},
|
||||||
beforeSend: this.setAuthorizationHeader as any,
|
beforeSend: this.setAuthorizationHeader,
|
||||||
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 as any,
|
beforeSend: this.setAuthorizationHeader,
|
||||||
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 as any,
|
beforeSend: this.setAuthorizationHeader,
|
||||||
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 as any,
|
beforeSend: this.setAuthorizationHeader,
|
||||||
cache: false,
|
cache: false,
|
||||||
}).then(
|
}).then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
@@ -882,11 +881,6 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
|
||||||
// All the following buttons require write access
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
|
const label = !this.isPreferredApiMongoDB ? "New Item" : "New Document";
|
||||||
if (this.newDocumentButton.visible()) {
|
if (this.newDocumentButton.visible()) {
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import {
|
|||||||
DetailsListLayoutMode,
|
DetailsListLayoutMode,
|
||||||
IColumn,
|
IColumn,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
|
||||||
Link,
|
Link,
|
||||||
Pivot,
|
Pivot,
|
||||||
PivotItem,
|
PivotItem,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
|
IconButton,
|
||||||
TooltipHost,
|
TooltipHost,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { HttpHeaders, NormalizedEventKey } from "Common/Constants";
|
import { HttpHeaders, NormalizedEventKey } from "Common/Constants";
|
||||||
@@ -18,15 +18,15 @@ import { QueryMetrics } from "Contracts/DataModels";
|
|||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import copy from "clipboard-copy";
|
|
||||||
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import CopilotCopy from "../../../../images/CopilotCopy.svg";
|
|
||||||
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
||||||
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
||||||
import RunQuery from "../../../../images/RunQuery.png";
|
import RunQuery from "../../../../images/RunQuery.png";
|
||||||
import InfoColor from "../../../../images/info_color.svg";
|
import InfoColor from "../../../../images/info_color.svg";
|
||||||
import { QueryResults } from "../../../Contracts/ViewModels";
|
import { QueryResults } from "../../../Contracts/ViewModels";
|
||||||
|
import copy from "clipboard-copy";
|
||||||
|
import CopilotCopy from "../../../../images/CopilotCopy.svg";
|
||||||
|
|
||||||
interface QueryResultProps {
|
interface QueryResultProps {
|
||||||
isMongoDB: boolean;
|
isMongoDB: boolean;
|
||||||
@@ -62,12 +62,9 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
|
|||||||
const columns: IColumn[] = [
|
const columns: IColumn[] = [
|
||||||
{
|
{
|
||||||
key: "column1",
|
key: "column1",
|
||||||
name: "Description",
|
name: "",
|
||||||
iconName: "Info",
|
|
||||||
isIconOnly: true,
|
|
||||||
minWidth: 10,
|
minWidth: 10,
|
||||||
maxWidth: 12,
|
maxWidth: 12,
|
||||||
iconClassName: "iconheadercell",
|
|
||||||
data: String,
|
data: String,
|
||||||
fieldName: "",
|
fieldName: "",
|
||||||
onRender: (item: IDocument) => {
|
onRender: (item: IDocument) => {
|
||||||
|
|||||||
@@ -91,6 +91,9 @@
|
|||||||
|
|
||||||
div[role="tabpanel"] {
|
div[role="tabpanel"] {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
div:nth-child(1) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-metadata {
|
.result-metadata {
|
||||||
@@ -280,6 +283,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.iconheadercell {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { FeedOptions, QueryOperationOptions } from "@azure/cosmos";
|
import { FeedOptions } 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";
|
||||||
@@ -11,7 +10,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, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } 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";
|
||||||
@@ -225,6 +224,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSaveQueryClick = (): void => {
|
public onSaveQueryClick = (): void => {
|
||||||
|
sessionStorage.setItem("focusedElementId", "savequery");
|
||||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -304,20 +304,8 @@ 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(
|
await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex);
|
||||||
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,
|
||||||
@@ -403,10 +391,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.saveQueryButton.visible && configContext.platform !== Platform.Fabric) {
|
if (this.saveQueryButton.visible) {
|
||||||
const label = "Save Query";
|
const label = "Save Query";
|
||||||
buttons.push({
|
buttons.push({
|
||||||
iconSrc: SaveQueryIcon,
|
iconSrc: SaveQueryIcon,
|
||||||
|
id: "savequery",
|
||||||
iconAlt: label,
|
iconAlt: label,
|
||||||
onCommandClick: this.onSaveQueryClick,
|
onCommandClick: this.onSaveQueryClick,
|
||||||
commandButtonLabel: label,
|
commandButtonLabel: label,
|
||||||
@@ -457,7 +446,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: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
|
ariaLabel: "Copilot",
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
};
|
};
|
||||||
buttons.push(toggleCopilotButton);
|
buttons.push(toggleCopilotButton);
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
|
||||||
import { sendMessage } from "Common/MessageHandler";
|
import { sendMessage } from "Common/MessageHandler";
|
||||||
import { Platform, configContext, updateConfigContext } from "ConfigContext";
|
|
||||||
import { IpRule } from "Contracts/DataModels";
|
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
@@ -13,9 +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 { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
|
||||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
@@ -34,13 +29,7 @@ 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" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(),
|
|
||||||
);
|
|
||||||
const [
|
|
||||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
|
||||||
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
|
||||||
return (
|
return (
|
||||||
<div className="tabsManagerContainer">
|
<div className="tabsManagerContainer">
|
||||||
{networkSettingsWarning && (
|
{networkSettingsWarning && (
|
||||||
@@ -65,39 +54,6 @@ 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".`}
|
|
||||||
<Link
|
|
||||||
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Link>
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
|
||||||
<MessageBar
|
|
||||||
messageBarType={MessageBarType.warning}
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please
|
|
||||||
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
|
||||||
</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">
|
||||||
@@ -323,59 +279,3 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
|
||||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
|
||||||
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
|
|
||||||
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
|
||||||
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
|
||||||
const ipRulesIncludeLegacyPortalBackend: boolean =
|
|
||||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule))
|
|
||||||
?.length === legacyPortalBackendIPs.length;
|
|
||||||
|
|
||||||
if (!ipRulesIncludeLegacyPortalBackend) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userContext.apiType === "Mongo") {
|
|
||||||
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
|
|
||||||
configContext.MONGO_PROXY_ENDPOINT,
|
|
||||||
);
|
|
||||||
|
|
||||||
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
|
|
||||||
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
|
|
||||||
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
|
|
||||||
|
|
||||||
const ipRulesIncludeMongoProxy: boolean =
|
|
||||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule))
|
|
||||||
?.length === mongoProxyOutboundIPs.length;
|
|
||||||
|
|
||||||
if (ipRulesIncludeMongoProxy) {
|
|
||||||
updateConfigContext({
|
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return !ipRulesIncludeMongoProxy;
|
|
||||||
} else if (userContext.apiType === "Cassandra") {
|
|
||||||
const isProdOrMpacCassandraProxyEndpoint: boolean = [
|
|
||||||
CassandraProxyEndpoints.Mpac,
|
|
||||||
CassandraProxyEndpoints.Prod,
|
|
||||||
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
|
|
||||||
|
|
||||||
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
|
|
||||||
? [
|
|
||||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
|
|
||||||
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
|
|
||||||
]
|
|
||||||
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
|
|
||||||
|
|
||||||
const ipRulesIncludeCassandraProxy: boolean =
|
|
||||||
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule))
|
|
||||||
?.length === cassandraProxyOutboundIPs.length;
|
|
||||||
|
|
||||||
return !ipRulesIncludeCassandraProxy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
private getTabId: () => string,
|
private getTabId: () => string,
|
||||||
private getUsername: () => string,
|
private getUsername: () => string,
|
||||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
||||||
private kind: ViewModels.TerminalKind,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
@@ -43,7 +42,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||||
screenshot={FirewallRuleScreenshot}
|
screenshot={FirewallRuleScreenshot}
|
||||||
shellName={this.getShellNameForDisplay(this.kind)}
|
shellName="PostgreSQL"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,18 +58,6 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
|||||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getShellNameForDisplay(terminalKind: ViewModels.TerminalKind): string {
|
|
||||||
switch (terminalKind) {
|
|
||||||
case ViewModels.TerminalKind.Postgres:
|
|
||||||
return "PostgreSQL";
|
|
||||||
case ViewModels.TerminalKind.Mongo:
|
|
||||||
case ViewModels.TerminalKind.VCoreMongo:
|
|
||||||
return "MongoDB";
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TerminalTab extends TabsBase {
|
export default class TerminalTab extends TabsBase {
|
||||||
@@ -89,7 +76,6 @@ export default class TerminalTab extends TabsBase {
|
|||||||
() => this.tabId,
|
() => this.tabId,
|
||||||
() => this.getUsername(),
|
() => this.getUsername(),
|
||||||
this.isAllPublicIPAddressesEnabled,
|
this.isAllPublicIPAddressesEnabled,
|
||||||
options.kind,
|
|
||||||
);
|
);
|
||||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -769,10 +769,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
|
|
||||||
const dataRootNode = buildDataTree();
|
const dataRootNode = buildDataTree();
|
||||||
const isSampleDataEnabled =
|
const isSampleDataEnabled =
|
||||||
useQueryCopilot().copilotEnabled &&
|
useQueryCopilot().copilotEnabled && userContext.sampleDataConnectionInfo && userContext.apiType === "SQL";
|
||||||
useQueryCopilot().copilotSampleDBEnabled &&
|
|
||||||
userContext.sampleDataConnectionInfo &&
|
|
||||||
userContext.apiType === "SQL";
|
|
||||||
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
|
const sampleDataResourceTokenCollection = useDatabases((state) => state.sampleDataResourceTokenCollection);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -68,9 +68,7 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
|
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
|
||||||
return isSampleDatabase === undefined
|
return get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
|
||||||
? get().databases.find((db) => databaseId === db.id())
|
|
||||||
: get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
|
|
||||||
},
|
},
|
||||||
isLastNonEmptyDatabase: () => {
|
isLastNonEmptyDatabase: () => {
|
||||||
const databases = get().databases;
|
const databases = get().databases;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { initializeIcons } from "@fluentui/react";
|
import { initializeIcons } from "@fluentui/react";
|
||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { render } from "react-dom";
|
import { render } from "react-dom";
|
||||||
import ChevronRight from "../images/chevron-right.svg";
|
import ChevronRight from "../images/chevron-right.svg";
|
||||||
@@ -33,8 +32,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
// For showing/hiding panel
|
// For showing/hiding panel
|
||||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth();
|
||||||
useAADAuth();
|
|
||||||
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||||
const [connectionString, setConnectionString] = React.useState<string>();
|
const [connectionString, setConnectionString] = React.useState<string>();
|
||||||
@@ -138,10 +136,7 @@ const App: React.FunctionComponent = () => {
|
|||||||
{!isLoggedIn && !encryptedTokenMetadata && (
|
{!isLoggedIn && !encryptedTokenMetadata && (
|
||||||
<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />
|
<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />
|
||||||
)}
|
)}
|
||||||
{isLoggedIn && authFailure && <AadAuthorizationFailure {...{ authFailure }} />}
|
{isLoggedIn && <DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />}
|
||||||
{isLoggedIn && !authFailure && (
|
|
||||||
<DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
|
||||||
import { GetGithubClientId } from "Utils/GitHubUtils";
|
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
|
import { GetGithubClientId } from "Utils/GitHubUtils";
|
||||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { configContext } from "ConfigContext";
|
|||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
import { useDialog } from "Explorer/Controls/Dialog";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import promiseRetry, { AbortError } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,48 +1,33 @@
|
|||||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
||||||
import { MessageTypes } from "Contracts/MessageTypes";
|
import { MessageTypes } from "Contracts/MessageTypes";
|
||||||
import { updateUserContext, userContext } from "UserContext";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
import { updateUserContext } from "UserContext";
|
||||||
|
|
||||||
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 during DEBOUNCE_DELAY_MS
|
// Prevents multiple parallel requests
|
||||||
let lastRequestTimestamp: number = undefined;
|
let isRequestPending = false;
|
||||||
|
|
||||||
const requestDatabaseResourceTokens = async (): Promise<void> => {
|
export const requestDatabaseResourceTokens = (): void => {
|
||||||
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
|
if (isRequestPending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRequestTimestamp = Date.now();
|
// TODO Make Fabric return the message id so we can handle this promise
|
||||||
try {
|
isRequestPending = true;
|
||||||
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
|
sendCachedDataMessage<FabricDatabaseConnectionInfo>(MessageTypes.GetAllResourceTokens, []);
|
||||||
MessageTypes.GetAllResourceTokens,
|
};
|
||||||
[],
|
|
||||||
userContext.fabricContext.connectionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!userContext.databaseAccount.properties.documentEndpoint) {
|
export const handleRequestDatabaseResourceTokensResponse = (
|
||||||
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
|
explorer: Explorer,
|
||||||
}
|
fabricDatabaseConnectionInfo: FabricDatabaseConnectionInfo,
|
||||||
|
): void => {
|
||||||
updateUserContext({
|
isRequestPending = false;
|
||||||
fabricContext: {
|
updateUserContext({ fabricDatabaseConnectionInfo });
|
||||||
...userContext.fabricContext,
|
scheduleRefreshDatabaseResourceToken();
|
||||||
databaseConnectionInfo: fabricDatabaseConnectionInfo,
|
explorer.refreshAllDatabases();
|
||||||
isReadOnly: true,
|
|
||||||
},
|
|
||||||
databaseAccount: { ...userContext.databaseAccount },
|
|
||||||
});
|
|
||||||
scheduleRefreshDatabaseResourceToken();
|
|
||||||
} catch (error) {
|
|
||||||
logConsoleError(error);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
lastRequestTimestamp = undefined;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,24 +35,19 @@ const requestDatabaseResourceTokens = async (): Promise<void> => {
|
|||||||
* @param tokenTimestamp
|
* @param tokenTimestamp
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
|
export const scheduleRefreshDatabaseResourceToken = (): void => {
|
||||||
return new Promise((resolve) => {
|
if (timeoutId !== undefined) {
|
||||||
if (timeoutId !== undefined) {
|
clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId);
|
timeoutId = undefined;
|
||||||
timeoutId = undefined;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
timeoutId = setTimeout(
|
timeoutId = setTimeout(() => {
|
||||||
() => {
|
requestDatabaseResourceTokens();
|
||||||
requestDatabaseResourceTokens().then(resolve);
|
}, TOKEN_VALIDITY_MS);
|
||||||
},
|
|
||||||
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()) {
|
||||||
scheduleRefreshDatabaseResourceToken(true);
|
requestDatabaseResourceTokens();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
.aadAuthFailureContainer {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.aadAuthFailureContainer .aadAuthFailureFormContainer {
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: -ms-flex;
|
|
||||||
display: flex;
|
|
||||||
-webkit-flex-direction: column;
|
|
||||||
-ms-flex-direction: column;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.aadAuthFailureContainer .aadAuthFailure {
|
|
||||||
text-align: center;
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: -ms-flex;
|
|
||||||
display: flex;
|
|
||||||
-webkit-flex-direction: column;
|
|
||||||
-ms-flex-direction: column;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
|
||||||
.aadAuthFailureContainer .aadAuthFailure .authFailureTitle {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #d12d2d;
|
|
||||||
margin: 16px 8px 8px 8px;
|
|
||||||
}
|
|
||||||
.aadAuthFailureContainer .aadAuthFailure .authFailureMessage {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #393939;
|
|
||||||
margin: 16px 16px 16px 16px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.aadAuthFailureContainer .aadAuthFailure .authFailureLink {
|
|
||||||
margin: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #0058ad;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aadAuthFailureContainer .aadAuthFailure .aadAuthFailureContent {
|
|
||||||
margin: 8px;
|
|
||||||
color: #393939;
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { AadAuthFailure } from "hooks/useAADAuth";
|
|
||||||
import * as React from "react";
|
|
||||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
|
||||||
import "../AadAuthorizationFailure.less";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
authFailure: AadAuthFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AadAuthorizationFailure: React.FunctionComponent<Props> = ({ authFailure }: Props) => {
|
|
||||||
return (
|
|
||||||
<div id="aadAuthFailure" className="aadAuthFailureContainer" style={{ display: "flex" }}>
|
|
||||||
<div className="aadAuthFailureFormContainer">
|
|
||||||
<div className="aadAuthFailure">
|
|
||||||
<p className="aadAuthFailureContent">
|
|
||||||
<img src={ConnectImage} alt="Azure Cosmos DB" />
|
|
||||||
</p>
|
|
||||||
<p className="authFailureTitle">Authorization Failure</p>
|
|
||||||
<p className="authFailureMessage">{authFailure.failureMessage}</p>
|
|
||||||
{authFailure.failureLinkTitle && (
|
|
||||||
<p className="authFailureLink" onClick={authFailure.failureLinkAction}>
|
|
||||||
{authFailure.failureLinkTitle}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -172,6 +172,7 @@ export class CollectionCreation {
|
|||||||
public static readonly DefaultCollectionRUs100K: number = 100000;
|
public static readonly DefaultCollectionRUs100K: number = 100000;
|
||||||
public static readonly DefaultCollectionRUs1Million: number = 1000000;
|
public static readonly DefaultCollectionRUs1Million: number = 1000000;
|
||||||
|
|
||||||
|
public static readonly DefaultAddCollectionDefaultFlight: string = "0";
|
||||||
public static readonly DefaultSubscriptionType: SubscriptionType = SubscriptionType.Free;
|
public static readonly DefaultSubscriptionType: SubscriptionType = SubscriptionType.Free;
|
||||||
|
|
||||||
public static readonly TablesAPIDefaultDatabase: string = "TablesDB";
|
public static readonly TablesAPIDefaultDatabase: string = "TablesDB";
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
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,
|
||||||
@@ -14,7 +11,6 @@ export enum StorageKey {
|
|||||||
MaxWaitTimeInSeconds,
|
MaxWaitTimeInSeconds,
|
||||||
AutomaticallyCancelQueryAfterTimeout,
|
AutomaticallyCancelQueryAfterTimeout,
|
||||||
ContainerPaginationEnabled,
|
ContainerPaginationEnabled,
|
||||||
CopilotSampleDBEnabled,
|
|
||||||
CustomItemPerPage,
|
CustomItemPerPage,
|
||||||
DatabaseAccountId,
|
DatabaseAccountId,
|
||||||
EncryptedKeyToken,
|
EncryptedKeyToken,
|
||||||
@@ -28,27 +24,3 @@ export enum StorageKey {
|
|||||||
VisitedAccounts,
|
VisitedAccounts,
|
||||||
PriorityLevel,
|
PriorityLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
|
||||||
const ruThresholdEnabledLocalStorageRaw: string | null = LocalStorageUtility.getEntryString(
|
|
||||||
StorageKey.RUThresholdEnabled,
|
|
||||||
);
|
|
||||||
return ruThresholdEnabledLocalStorageRaw === "true" || ruThresholdEnabledLocalStorageRaw === "false";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ruThresholdEnabled = (): boolean => {
|
|
||||||
const ruThresholdEnabledLocalStorageRaw: string | null = LocalStorageUtility.getEntryString(
|
|
||||||
StorageKey.RUThresholdEnabled,
|
|
||||||
);
|
|
||||||
return ruThresholdEnabledLocalStorageRaw === null || StringUtility.toBoolean(ruThresholdEnabledLocalStorageRaw);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRUThreshold = (): number => {
|
|
||||||
const ruThresholdRaw = LocalStorageUtility.getEntryNumber(StorageKey.RUThreshold);
|
|
||||||
if (ruThresholdRaw !== 0) {
|
|
||||||
return ruThresholdRaw;
|
|
||||||
}
|
|
||||||
return DefaultRUThreshold;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DefaultRUThreshold = 5000;
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricContract";
|
||||||
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,14 +47,8 @@ export interface VCoreMongoConnectionParams {
|
|||||||
connectionString: string;
|
connectionString: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FabricContext {
|
|
||||||
connectionId: string;
|
|
||||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
|
||||||
isReadOnly: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
readonly fabricContext?: FabricContext;
|
readonly fabricDatabaseConnectionInfo?: FabricDatabaseConnectionInfo;
|
||||||
readonly authType?: AuthType;
|
readonly authType?: AuthType;
|
||||||
readonly masterKey?: string;
|
readonly masterKey?: string;
|
||||||
readonly subscriptionId?: string;
|
readonly subscriptionId?: string;
|
||||||
@@ -73,6 +67,7 @@ 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;
|
||||||
@@ -87,7 +82,7 @@ interface UserContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||||
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod1" | "rx" | "ex" | "prod" | "dev";
|
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev";
|
||||||
|
|
||||||
const ONE_WEEK_IN_MS = 604800000;
|
const ONE_WEEK_IN_MS = 604800000;
|
||||||
|
|
||||||
@@ -99,6 +94,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||||
@@ -45,8 +43,8 @@ export function decryptJWTToken(token: string) {
|
|||||||
return JSON.parse(tokenPayload);
|
return JSON.parse(tokenPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMsalInstance() {
|
export function getMsalInstance() {
|
||||||
const msalConfig: msal.Configuration = {
|
const config: msal.Configuration = {
|
||||||
cache: {
|
cache: {
|
||||||
cacheLocation: "localStorage",
|
cacheLocation: "localStorage",
|
||||||
},
|
},
|
||||||
@@ -57,46 +55,8 @@ export async function getMsalInstance() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
|
config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
|
||||||
}
|
}
|
||||||
|
const msalInstance = new msal.PublicClientApplication(config);
|
||||||
const msalInstance = new msal.PublicClientApplication(msalConfig);
|
|
||||||
return msalInstance;
|
return msalInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) {
|
|
||||||
const tokenRequest = {
|
|
||||||
account: msalInstance.getActiveAccount() || null,
|
|
||||||
...request,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// attempt silent acquisition first
|
|
||||||
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
|
|
||||||
} catch (silentError) {
|
|
||||||
if (silentError instanceof msal.InteractionRequiredAuthError) {
|
|
||||||
try {
|
|
||||||
// The error indicates that we need to acquire the token interactively.
|
|
||||||
// This will display a pop-up to re-establish authorization. If user does not
|
|
||||||
// have pop-ups enabled in their browser, this will fail.
|
|
||||||
return (await msalInstance.acquireTokenPopup(tokenRequest)).accessToken;
|
|
||||||
} catch (interactiveError) {
|
|
||||||
traceFailure(Action.SignInAad, {
|
|
||||||
request: JSON.stringify(tokenRequest),
|
|
||||||
acquireTokenType: "interactive",
|
|
||||||
errorMessage: JSON.stringify(interactiveError),
|
|
||||||
});
|
|
||||||
|
|
||||||
throw interactiveError;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
traceFailure(Action.SignInAad, {
|
|
||||||
request: JSON.stringify(tokenRequest),
|
|
||||||
acquireTokenType: "silent",
|
|
||||||
errorMessage: JSON.stringify(silentError),
|
|
||||||
});
|
|
||||||
|
|
||||||
throw silentError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export function isRunningOnNationalCloud(): boolean {
|
export function isRunningOnNationalCloud(): boolean {
|
||||||
return !isRunningOnPublicCloud();
|
return (
|
||||||
}
|
userContext.portalEnv === "blackforest" ||
|
||||||
|
userContext.portalEnv === "fairfax" ||
|
||||||
export function isRunningOnPublicCloud(): boolean {
|
userContext.portalEnv === "mooncake"
|
||||||
return userContext?.portalEnv === "prod1" || userContext?.portalEnv === "prod";
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
import { JunoEndpoints } from "Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
|
|
||||||
export function validateEndpoint(
|
export function validateEndpoint(
|
||||||
@@ -67,22 +67,7 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
|||||||
//usnat: ["7.28.202.68"],
|
//usnat: ["7.28.202.68"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
|
|
||||||
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
|
|
||||||
[MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"],
|
|
||||||
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
|
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
||||||
@@ -90,21 +75,6 @@ export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> =
|
|||||||
"https://localhost:12901",
|
"https://localhost:12901",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const allowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
|
||||||
CassandraProxyEndpoints.Development,
|
|
||||||
CassandraProxyEndpoints.Mpac,
|
|
||||||
CassandraProxyEndpoints.Prod,
|
|
||||||
CassandraProxyEndpoints.Fairfax,
|
|
||||||
CassandraProxyEndpoints.Mooncake,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const CassandraProxyOutboundIPs: { [key: string]: string[] } = {
|
|
||||||
[CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"],
|
|
||||||
[CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"],
|
|
||||||
[CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"],
|
|
||||||
[CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
||||||
|
|
||||||
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
||||||
@@ -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/EndpointUtils";
|
import { PortalBackendIPs } from "Utils/EndpointValidation";
|
||||||
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
|
import { getNetworkSettingsWarningMessage } from "./NetworkUtility";
|
||||||
|
|
||||||
describe("NetworkUtility tests", () => {
|
describe("NetworkUtility tests", () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { configContext } from "ConfigContext";
|
import { configContext } from "ConfigContext";
|
||||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { PortalBackendIPs } from "Utils/EndpointUtils";
|
import { PortalBackendIPs } from "Utils/EndpointValidation";
|
||||||
|
|
||||||
export const getNetworkSettingsWarningMessage = async (
|
export const getNetworkSettingsWarningMessage = async (
|
||||||
setStateFunc: (warningMessage: string) => void,
|
setStateFunc: (warningMessage: string) => void,
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
AUTOGENERATED FILE
|
|
||||||
Run "npm run generateARMClients" to regenerate
|
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { configContext } from "../../../../ConfigContext";
|
|
||||||
import { armRequest } from "../../request";
|
|
||||||
import * as Types from "./types";
|
|
||||||
const apiVersion = "2023-11-15-preview";
|
|
||||||
|
|
||||||
/* Creates a Data Transfer Job. */
|
|
||||||
export async function create(
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
jobName: string,
|
|
||||||
body: Types.CreateJobRequest,
|
|
||||||
): Promise<Types.DataTransferJobGetResults> {
|
|
||||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`;
|
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get a Data Transfer Job. */
|
|
||||||
export async function get(
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
jobName: string,
|
|
||||||
): Promise<Types.DataTransferJobGetResults> {
|
|
||||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}`;
|
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pause a Data Transfer Job. */
|
|
||||||
export async function pause(
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
jobName: string,
|
|
||||||
): Promise<Types.DataTransferJobGetResults> {
|
|
||||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/pause`;
|
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resumes a Data Transfer Job. */
|
|
||||||
export async function resume(
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
jobName: string,
|
|
||||||
): Promise<Types.DataTransferJobGetResults> {
|
|
||||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/resume`;
|
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cancels a Data Transfer Job. */
|
|
||||||
export async function cancel(
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
jobName: string,
|
|
||||||
): Promise<Types.DataTransferJobGetResults> {
|
|
||||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs/${jobName}/cancel`;
|
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get a list of Data Transfer jobs. */
|
|
||||||
export async function listByDatabaseAccount(
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroupName: string,
|
|
||||||
accountName: string,
|
|
||||||
): Promise<Types.DataTransferJobFeedResults> {
|
|
||||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
|
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
AUTOGENERATED FILE
|
|
||||||
Run "npm run generateARMClients" to regenerate
|
|
||||||
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
|
|
||||||
|
|
||||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-11-15-preview/dataTransferService.json
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Base class for all DataTransfer source/sink */
|
|
||||||
export interface DataTransferDataSourceSink {
|
|
||||||
/* undocumented */
|
|
||||||
component: "CosmosDBCassandra" | "CosmosDBMongo" | "CosmosDBSql" | "AzureBlobStorage";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* A base CosmosDB data source/sink */
|
|
||||||
export type BaseCosmosDataTransferDataSourceSink = DataTransferDataSourceSink & {
|
|
||||||
/* undocumented */
|
|
||||||
remoteAccountName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* A CosmosDB Cassandra API data source/sink */
|
|
||||||
export type CosmosCassandraDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
|
|
||||||
/* undocumented */
|
|
||||||
keyspaceName: string;
|
|
||||||
/* undocumented */
|
|
||||||
tableName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* A CosmosDB Mongo API data source/sink */
|
|
||||||
export type CosmosMongoDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
|
|
||||||
/* undocumented */
|
|
||||||
databaseName: string;
|
|
||||||
/* undocumented */
|
|
||||||
collectionName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* A CosmosDB No Sql API data source/sink */
|
|
||||||
export type CosmosSqlDataTransferDataSourceSink = BaseCosmosDataTransferDataSourceSink & {
|
|
||||||
/* undocumented */
|
|
||||||
databaseName: string;
|
|
||||||
/* undocumented */
|
|
||||||
containerName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* An Azure Blob Storage data source/sink */
|
|
||||||
export type AzureBlobDataTransferDataSourceSink = DataTransferDataSourceSink & {
|
|
||||||
/* undocumented */
|
|
||||||
containerName: string;
|
|
||||||
/* undocumented */
|
|
||||||
endpointUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* The properties of a DataTransfer Job */
|
|
||||||
export interface DataTransferJobProperties {
|
|
||||||
/* Job Name */
|
|
||||||
readonly jobName?: string;
|
|
||||||
/* Source DataStore details */
|
|
||||||
source: DataTransferDataSourceSink;
|
|
||||||
|
|
||||||
/* Destination DataStore details */
|
|
||||||
destination: DataTransferDataSourceSink;
|
|
||||||
|
|
||||||
/* Job Status */
|
|
||||||
readonly status?: string;
|
|
||||||
/* Processed Count. */
|
|
||||||
readonly processedCount?: number;
|
|
||||||
/* Total Count. */
|
|
||||||
readonly totalCount?: number;
|
|
||||||
/* Last Updated Time (ISO-8601 format). */
|
|
||||||
readonly lastUpdatedUtcTime?: string;
|
|
||||||
/* Worker count */
|
|
||||||
workerCount?: number;
|
|
||||||
/* Error response for Faulted job */
|
|
||||||
readonly error?: unknown;
|
|
||||||
|
|
||||||
/* Total Duration of Job */
|
|
||||||
readonly duration?: string;
|
|
||||||
/* Mode of job execution */
|
|
||||||
mode?: "Offline" | "Online";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Parameters to create Data Transfer Job */
|
|
||||||
export type CreateJobRequest = unknown & {
|
|
||||||
/* Data Transfer Create Job Properties */
|
|
||||||
properties: DataTransferJobProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* A Cosmos DB Data Transfer Job */
|
|
||||||
export type DataTransferJobGetResults = unknown & {
|
|
||||||
/* undocumented */
|
|
||||||
properties?: DataTransferJobProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* The List operation response, that contains the Data Transfer jobs and their properties. */
|
|
||||||
export interface DataTransferJobFeedResults {
|
|
||||||
/* List of Data Transfer jobs and their properties. */
|
|
||||||
readonly value?: DataTransferJobGetResults[];
|
|
||||||
|
|
||||||
/* URL to get the next set of Data Transfer job list results if there are any. */
|
|
||||||
readonly nextLink?: string;
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,9 @@ import * as msal from "@azure/msal-browser";
|
|||||||
import { useBoolean } from "@fluentui/react-hooks";
|
import { useBoolean } from "@fluentui/react-hooks";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { acquireTokenWithMsal, getMsalInstance } from "../Utils/AuthorizationUtils";
|
import { getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||||
|
|
||||||
const msalInstance = await getMsalInstance();
|
const msalInstance = getMsalInstance();
|
||||||
|
|
||||||
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
const cachedAccount = msalInstance.getAllAccounts()?.[0];
|
||||||
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
const cachedTenantId = localStorage.getItem("cachedTenantId");
|
||||||
@@ -18,13 +18,6 @@ interface ReturnType {
|
|||||||
tenantId: string;
|
tenantId: string;
|
||||||
account: msal.AccountInfo;
|
account: msal.AccountInfo;
|
||||||
switchTenant: (tenantId: string) => void;
|
switchTenant: (tenantId: string) => void;
|
||||||
authFailure: AadAuthFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AadAuthFailure {
|
|
||||||
failureMessage: string;
|
|
||||||
failureLinkTitle?: string;
|
|
||||||
failureLinkAction?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAADAuth(): ReturnType {
|
export function useAADAuth(): ReturnType {
|
||||||
@@ -35,7 +28,6 @@ export function useAADAuth(): ReturnType {
|
|||||||
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
|
||||||
const [graphToken, setGraphToken] = React.useState<string>();
|
const [graphToken, setGraphToken] = React.useState<string>();
|
||||||
const [armToken, setArmToken] = React.useState<string>();
|
const [armToken, setArmToken] = React.useState<string>();
|
||||||
const [authFailure, setAuthFailure] = React.useState<AadAuthFailure>(undefined);
|
|
||||||
|
|
||||||
msalInstance.setActiveAccount(account);
|
msalInstance.setActiveAccount(account);
|
||||||
const login = React.useCallback(async () => {
|
const login = React.useCallback(async () => {
|
||||||
@@ -69,60 +61,24 @@ export function useAADAuth(): ReturnType {
|
|||||||
[account, tenantId],
|
[account, tenantId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const acquireTokens = React.useCallback(async () => {
|
React.useEffect(() => {
|
||||||
if (!(account && tenantId)) {
|
if (account && tenantId) {
|
||||||
return;
|
Promise.all([
|
||||||
}
|
msalInstance.acquireTokenSilent({
|
||||||
|
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
||||||
try {
|
scopes: [`${configContext.GRAPH_ENDPOINT}/.default`],
|
||||||
const armToken = await acquireTokenWithMsal(msalInstance, {
|
}),
|
||||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
msalInstance.acquireTokenSilent({
|
||||||
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
||||||
|
scopes: [`${configContext.ARM_ENDPOINT}/.default`],
|
||||||
|
}),
|
||||||
|
]).then(([graphTokenResponse, armTokenResponse]) => {
|
||||||
|
setGraphToken(graphTokenResponse.accessToken);
|
||||||
|
setArmToken(armTokenResponse.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
setArmToken(armToken);
|
|
||||||
setAuthFailure(null);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof msal.AuthError && error.errorCode === msal.BrowserAuthErrorMessage.popUpWindowError.code) {
|
|
||||||
// This error can occur when acquireTokenWithMsal() has attempted to acquire token interactively
|
|
||||||
// and user has popups disabled in browser. This fails as the popup is not the result of a explicit user
|
|
||||||
// action. In this case, we display the failure and a link to repeat the operation. Clicking on the
|
|
||||||
// link is a user action so it will work even if popups have been disabled.
|
|
||||||
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/76#issuecomment-324787539
|
|
||||||
setAuthFailure({
|
|
||||||
failureMessage:
|
|
||||||
"We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease click below to retry authorization without requiring popups being enabled.",
|
|
||||||
failureLinkTitle: "Retry Authorization",
|
|
||||||
failureLinkAction: acquireTokens,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const errorJson = JSON.stringify(error);
|
|
||||||
setAuthFailure({
|
|
||||||
failureMessage: `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const graphToken = await acquireTokenWithMsal(msalInstance, {
|
|
||||||
authority: `${configContext.AAD_ENDPOINT}${tenantId}`,
|
|
||||||
scopes: [`${configContext.GRAPH_ENDPOINT}/.default`],
|
|
||||||
});
|
|
||||||
|
|
||||||
setGraphToken(graphToken);
|
|
||||||
} catch (error) {
|
|
||||||
// Graph token is used only for retrieving user photo at the moment, so
|
|
||||||
// it's not critical if this fails.
|
|
||||||
console.warn("Error acquiring graph token: " + error);
|
|
||||||
}
|
}
|
||||||
}, [account, tenantId]);
|
}, [account, tenantId]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (account && tenantId && !authFailure) {
|
|
||||||
acquireTokens();
|
|
||||||
}
|
|
||||||
}, [account, tenantId, acquireTokens, authFailure]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account,
|
account,
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -132,6 +88,5 @@ export function useAADAuth(): ReturnType {
|
|||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
switchTenant,
|
switchTenant,
|
||||||
authFailure,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { getDataTransferJobs } from "Common/dataAccess/dataTransfers";
|
|
||||||
import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types";
|
|
||||||
import create, { UseStore } from "zustand";
|
|
||||||
|
|
||||||
export interface DataTransferJobsState {
|
|
||||||
dataTransferJobs: DataTransferJobGetResults[];
|
|
||||||
pollingDataTransferJobs: Set<string>;
|
|
||||||
setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => void;
|
|
||||||
setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataTransferJobStore = UseStore<DataTransferJobsState>;
|
|
||||||
|
|
||||||
export const useDataTransferJobs: DataTransferJobStore = create((set) => ({
|
|
||||||
dataTransferJobs: [],
|
|
||||||
pollingDataTransferJobs: new Set<string>(),
|
|
||||||
setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => set({ dataTransferJobs }),
|
|
||||||
setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => set({ pollingDataTransferJobs }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const refreshDataTransferJobs = async (
|
|
||||||
subscriptionId: string,
|
|
||||||
resourceGroup: string,
|
|
||||||
accountName: string,
|
|
||||||
): Promise<DataTransferJobGetResults[]> => {
|
|
||||||
const dataTransferJobs: DataTransferJobGetResults[] = await getDataTransferJobs(
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroup,
|
|
||||||
accountName,
|
|
||||||
);
|
|
||||||
const jobRegex = /^Portal_(.+)_(\d{10,})$/;
|
|
||||||
const sortedJobs: DataTransferJobGetResults[] = dataTransferJobs?.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b?.properties?.lastUpdatedUtcTime).getTime() - new Date(a?.properties?.lastUpdatedUtcTime).getTime(),
|
|
||||||
);
|
|
||||||
const filteredJobs = sortedJobs.filter((job) => jobRegex.test(job?.properties?.jobName));
|
|
||||||
useDataTransferJobs.getState().setDataTransferJobs(filteredJobs);
|
|
||||||
return filteredJobs;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateDataTransferJob = (updateJob: DataTransferJobGetResults) => {
|
|
||||||
const updatedDataTransferJobs = useDataTransferJobs
|
|
||||||
.getState()
|
|
||||||
.dataTransferJobs.map((job: DataTransferJobGetResults) =>
|
|
||||||
job?.properties?.jobName === updateJob?.properties?.jobName ? updateJob : job,
|
|
||||||
);
|
|
||||||
useDataTransferJobs.getState().setDataTransferJobs(updatedDataTransferJobs);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addToPolling = (addJob: string) => {
|
|
||||||
const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
|
|
||||||
pollingJobs.add(addJob);
|
|
||||||
useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeFromPolling = (removeJob: string) => {
|
|
||||||
const pollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
|
|
||||||
pollingJobs.delete(removeJob);
|
|
||||||
useDataTransferJobs.getState().setPollingDataTransferJobs(pollingJobs);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user