mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-31 23:02:29 +00:00
Compare commits
29 Commits
before_opt
...
users/ajpa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a9f8c3e32 | ||
|
|
5a36a6b45d | ||
|
|
32576f50d3 | ||
|
|
10f5a5fbfe | ||
|
|
8eb53674dc | ||
|
|
257256f915 | ||
|
|
41f5401016 | ||
|
|
a4c9a47d4e | ||
|
|
c43132d5c0 | ||
|
|
6ce81099ef | ||
|
|
777e411f4f | ||
|
|
63d4b4f4ef | ||
|
|
eaf9a14e7d | ||
|
|
4b65760a1d | ||
|
|
ced2725476 | ||
|
|
b5d7423849 | ||
|
|
1529303107 | ||
|
|
083bccfda9 | ||
|
|
14c9874e5e | ||
|
|
a04eaff6be | ||
|
|
51a412e2c0 | ||
|
|
3fcbdf6152 | ||
|
|
8da078579e | ||
|
|
4ac41031e6 | ||
|
|
d7923db108 | ||
|
|
0170c9e1cc | ||
|
|
2730da7ab6 | ||
|
|
de2449ee25 | ||
|
|
99378582ce |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -21,3 +21,5 @@ GettingStarted-ignore*.ipynb
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/.vs/cosmos-explorer
|
||||
/.vs/slnx.sqlite-journal
|
||||
|
||||
BIN
.vs/slnx.sqlite
BIN
.vs/slnx.sqlite
Binary file not shown.
@@ -2,7 +2,6 @@
|
||||
|
||||
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
||||
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
|
||||
@GalleryBackgroundColor: #fdfdfd;
|
||||
|
||||
@LinkColor: #2d6da4;
|
||||
|
||||
//Icons
|
||||
@InfoIconColor: #0072c6;
|
||||
@WarningIconColor: #db7500;
|
||||
@@ -246,6 +248,10 @@
|
||||
outline: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
.focusedBorder() {
|
||||
border: 1px dashed @FocusColor;
|
||||
}
|
||||
|
||||
/************************************************************************************************
|
||||
Common Toggle Switch
|
||||
*************************************************************************************************/
|
||||
|
||||
@@ -1914,13 +1914,20 @@ input::-webkit-calendar-picker-indicator::after {
|
||||
}
|
||||
|
||||
.nav-tabs-margin {
|
||||
height: 32px;
|
||||
background-color: #f2f2f2;
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
margin-bottom: -0.5px;
|
||||
|
||||
li {
|
||||
// Override the bootstrap defaults here to align with our layout constants.
|
||||
margin-bottom: 0px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
package-lock.json
generated
51
package-lock.json
generated
@@ -86,7 +86,7 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.6.2",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
@@ -12662,7 +12662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
|
||||
"integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sanitize-html": {
|
||||
@@ -21799,6 +21801,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz",
|
||||
"integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
@@ -30243,14 +30257,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
|
||||
"integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"@types/retry": "0.12.2",
|
||||
"is-network-error": "^1.0.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
@@ -35997,6 +36017,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/@types/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ajv": {
|
||||
"version": "8.12.0",
|
||||
"dev": true,
|
||||
@@ -36044,6 +36071,20 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/p-retry": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/retry": "0.12.0",
|
||||
"retry": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"dev": true,
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "4.6.2",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
|
||||
37913
preview/package-lock.json
generated
37913
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -530,6 +530,10 @@ export class ariaLabelForLearnMoreLink {
|
||||
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
|
||||
}
|
||||
|
||||
export class FeedbackLabels {
|
||||
public static readonly provideFeedback: string = "Provide feedback";
|
||||
}
|
||||
|
||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
|
||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
|
||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BackendApi, PriorityLevel } from "../Common/Constants";
|
||||
import { PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
@@ -19,7 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||
Logger.logInfo(
|
||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||
@@ -42,7 +43,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
return decodeURIComponent(headers.authorization);
|
||||
}
|
||||
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
if (isFabricMirroredKey()) {
|
||||
switch (requestInfo.resourceType) {
|
||||
case Cosmos.ResourceType.conflicts:
|
||||
case Cosmos.ResourceType.container:
|
||||
@@ -54,8 +55,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
// User resource tokens
|
||||
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined
|
||||
headers[HttpHeaders.msDate] = new Date().toUTCString();
|
||||
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||
const resourceTokens = (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens;
|
||||
checkDatabaseResourceTokensValidity(
|
||||
(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.resourceTokenInfo.resourceTokensTimestamp,
|
||||
);
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
case Cosmos.ResourceType.none:
|
||||
@@ -66,7 +72,9 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
// For now, these operations aren't used, so fetching the authorization token is commented out.
|
||||
// This provider must return a real token to pass validation by the client, so we return the cached resource token
|
||||
// (which is a valid token, but won't work for these operations).
|
||||
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||
const resourceTokens2 = (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens;
|
||||
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
|
||||
|
||||
/* ************** TODO: Uncomment this code if we need to support these operations **************
|
||||
@@ -125,10 +133,6 @@ export async function getTokenFromAuthService(
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
|
||||
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
|
||||
}
|
||||
|
||||
try {
|
||||
const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", {
|
||||
@@ -151,34 +155,6 @@ export async function getTokenFromAuthService(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTokenFromAuthService_ToBeDeprecated(
|
||||
verb: string,
|
||||
resourceType: string,
|
||||
resourceId?: string,
|
||||
): Promise<AuthorizationToken> {
|
||||
try {
|
||||
const host = configContext.BACKEND_ENDPOINT;
|
||||
const response = await _global.fetch(host + "/api/guest/runtimeproxy/authorizationTokens", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-ms-encrypted-auth-token": userContext.accessToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
verb,
|
||||
resourceType,
|
||||
resourceId,
|
||||
}),
|
||||
});
|
||||
//TODO I am not sure why we have to parse the JSON again here. fetch should do it for us when we call .json()
|
||||
const result = JSON.parse(await response.json());
|
||||
return result;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||
enum SDKSupportedCapabilities {
|
||||
None = 0,
|
||||
@@ -203,8 +179,10 @@ export function client(): Cosmos.CosmosClient {
|
||||
}
|
||||
|
||||
let _defaultHeaders: Cosmos.CosmosHeaders = {};
|
||||
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
|
||||
|
||||
if (
|
||||
userContext.authType === AuthType.ConnectionString ||
|
||||
|
||||
@@ -4,16 +4,8 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@@ -196,20 +188,8 @@ describe("MongoProxyClient", () => {
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_BACKEND_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
updateDocument(databaseId, collection, documentId, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("deleteDocument", () => {
|
||||
describe("deleteDocuments", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateUserContext({
|
||||
@@ -226,9 +206,9 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("builds the correct URL", () => {
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -238,9 +218,9 @@ describe("MongoProxyClient", () => {
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
deleteDocument(databaseId, collection, documentId);
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
@@ -275,33 +255,4 @@ describe("MongoProxyClient", () => {
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFeatureEndpointOrDefault", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
"feature.mongoProxyEndpoint": MongoProxyEndpoints.Prod,
|
||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||
});
|
||||
const features = extractFeatures(params);
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
features: features,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a local endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("DeleteDocument");
|
||||
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import {
|
||||
allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
import queryString from "querystring";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@@ -67,10 +60,6 @@ export function queryDocuments(
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
|
||||
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
|
||||
}
|
||||
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
@@ -89,7 +78,7 @@ export function queryDocuments(
|
||||
query,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -127,76 +116,11 @@ export function queryDocuments(
|
||||
});
|
||||
}
|
||||
|
||||
function queryDocuments_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
isResourceList: boolean,
|
||||
query: string,
|
||||
continuationToken?: string,
|
||||
): Promise<QueryResponse> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||
rid: collection.rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
collection && collection.partitionKey && !collection.partitionKey.systemKey
|
||||
? collection.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
|
||||
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}?${queryString.stringify(params)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ query }),
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
export function readDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
|
||||
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@@ -217,7 +141,7 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -237,61 +161,12 @@ export function readDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function readDocument_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
): Promise<DataModels.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 = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: encodeURIComponent(
|
||||
JSON.stringify(documentId.partitionKeyHeader()),
|
||||
),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "reading document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function createDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
partitionKeyProperty: string,
|
||||
documentContent: unknown,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
|
||||
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
@@ -308,7 +183,7 @@ export function createDocument(
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createDocument`, {
|
||||
@@ -328,54 +203,12 @@ export function createDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function createDocument_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
partitionKeyProperty: string,
|
||||
documentContent: unknown,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const params = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}dbs/${databaseId}/colls/${collection.id()}/docs/`,
|
||||
rid: collection.rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(documentContent),
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "creating document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
documentContent: string,
|
||||
): Promise<DataModels.DocumentId> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
|
||||
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||
const idComponents = documentId.self.split("/");
|
||||
@@ -396,7 +229,7 @@ export function updateDocument(
|
||||
: "",
|
||||
documentContent,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(endpoint, {
|
||||
@@ -417,139 +250,6 @@ export function updateDocument(
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDocument_ToBeDeprecated(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
documentId: DocumentId,
|
||||
documentContent: string,
|
||||
): Promise<DataModels.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 = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "PUT",
|
||||
body: documentContent,
|
||||
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 deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.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(MongoProxyApi.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 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 = {
|
||||
db: databaseId,
|
||||
coll: collection.id(),
|
||||
resourceUrl: `${resourceEndpoint}${path}/${rid}`,
|
||||
rid,
|
||||
rtype: "docs",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||
[CosmosSDKConstants.HttpHeaders.PartitionKey]: JSON.stringify(documentId.partitionKeyHeader()),
|
||||
},
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return undefined;
|
||||
}
|
||||
return await errorHandling(response, "deleting document", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDocuments(
|
||||
databaseId: string,
|
||||
collection: Collection,
|
||||
@@ -575,7 +275,7 @@ export function deleteDocuments(
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
};
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/bulkdelete`, {
|
||||
@@ -599,9 +299,6 @@ export function deleteDocuments(
|
||||
export function createMongoCollectionWithProxy(
|
||||
params: DataModels.CreateCollectionParams,
|
||||
): Promise<DataModels.Collection> {
|
||||
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
|
||||
return createMongoCollectionWithProxy_ToBeDeprecated(params);
|
||||
}
|
||||
const { databaseAccount } = userContext;
|
||||
const shardKey: string = params.partitionKey?.paths[0];
|
||||
|
||||
@@ -622,7 +319,7 @@ export function createMongoCollectionWithProxy(
|
||||
isSharded: !!shardKey,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/createCollection`, {
|
||||
@@ -642,70 +339,6 @@ export function createMongoCollectionWithProxy(
|
||||
});
|
||||
}
|
||||
|
||||
export function createMongoCollectionWithProxy_ToBeDeprecated(
|
||||
params: DataModels.CreateCollectionParams,
|
||||
): Promise<DataModels.Collection> {
|
||||
const { databaseAccount } = userContext;
|
||||
const shardKey: string = params.partitionKey?.paths[0];
|
||||
const mongoParams: DataModels.MongoParameters = {
|
||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||
db: params.databaseId,
|
||||
coll: params.collectionId,
|
||||
pk: shardKey,
|
||||
offerThroughput: params.autoPilotMaxThroughput || params.offerThroughput,
|
||||
cd: params.createNewDatabase,
|
||||
st: params.databaseLevelThroughput,
|
||||
is: !!shardKey,
|
||||
rid: "",
|
||||
rtype: "colls",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
isAutoPilot: !!params.autoPilotMaxThroughput,
|
||||
};
|
||||
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
|
||||
return window
|
||||
.fetch(
|
||||
`${endpoint}/createCollection?${queryString.stringify(
|
||||
mongoParams as unknown as queryString.ParsedUrlQueryInput,
|
||||
)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...authHeaders(),
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return await errorHandling(response, "creating collection", mongoParams);
|
||||
});
|
||||
}
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
let endpoint;
|
||||
if (useMongoProxyEndpoint(feature)) {
|
||||
endpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
} else {
|
||||
const allowedMongoProxyEndpoints = configContext.allowedMongoProxyEndpoints || [
|
||||
...defaultAllowedMongoProxyEndpoints,
|
||||
...allowedMongoProxyEndpoints_ToBeDeprecated,
|
||||
];
|
||||
endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
return getEndpoint(endpoint);
|
||||
}
|
||||
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
@@ -719,84 +352,6 @@ export function getEndpoint(endpoint: string): string {
|
||||
return url;
|
||||
}
|
||||
|
||||
export function useMongoProxyEndpoint(mongoProxyApi: string): boolean {
|
||||
const mongoProxyEnvironmentMap: { [key: string]: string[] } = {
|
||||
[MongoProxyApi.ResourceList]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.QueryDocuments]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.ReadDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.UpdateDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.DeleteDocument]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.CreateCollectionWithProxy]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.LegacyMongoShell]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
[MongoProxyApi.BulkDelete]: [
|
||||
MongoProxyEndpoints.Development,
|
||||
MongoProxyEndpoints.Mpac,
|
||||
MongoProxyEndpoints.Prod,
|
||||
MongoProxyEndpoints.Fairfax,
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
],
|
||||
};
|
||||
|
||||
if (!mongoProxyEnvironmentMap[mongoProxyApi] || !configContext.MONGO_PROXY_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configContext.globallyEnabledMongoAPIs.includes(mongoProxyApi)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return mongoProxyEnvironmentMap[mongoProxyApi].includes(configContext.MONGO_PROXY_ENDPOINT);
|
||||
}
|
||||
|
||||
export class ThrottlingError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
|
||||
|
||||
export function updateStyles(): void {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
if (isFabric()) {
|
||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
@@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
);
|
||||
try {
|
||||
let collection: DataModels.Collection;
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (params.createNewDatabase) {
|
||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
@@ -12,7 +13,7 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||
try {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||
await deleteCollectionWithARM(databaseId, collectionId);
|
||||
} else {
|
||||
await client().database(databaseId).container(collectionId).delete();
|
||||
|
||||
@@ -105,6 +105,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
? parseInt(resource.softAllowedMaximumThroughput)
|
||||
: resource.softAllowedMaximumThroughput;
|
||||
|
||||
const throughputBuckets = resource?.throughputBuckets;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
@@ -114,6 +116,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
throughputBuckets,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,6 +128,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
throughputBuckets,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ContainerResponse } from "@azure/cosmos";
|
||||
import { Queries } from "Common/Constants";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
@@ -16,15 +17,13 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
userContext.fabricContext &&
|
||||
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
|
||||
) {
|
||||
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
||||
const collections: DataModels.Collection[] = [];
|
||||
const promises: Promise<ContainerResponse>[] = [];
|
||||
|
||||
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||
for (const collectionResourceId in (
|
||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
||||
).resourceTokenInfo.resourceTokens) {
|
||||
// Dictionary key looks like this: dbs/SampleDB/colls/Container
|
||||
const resourceIdObj = collectionResourceId.split("/");
|
||||
const tokenDatabaseId = resourceIdObj[1];
|
||||
@@ -56,7 +55,8 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
return await readCollectionsWithARM(databaseId);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -11,8 +11,9 @@ import { handleError } from "../ErrorHandlingUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
|
||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||
if (isFabricMirroredKey() || isFabricNative()) {
|
||||
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
||||
// For Fabric Native, it is not supported.
|
||||
console.error("Skiping readDatabaseOffer for Fabric");
|
||||
return undefined;
|
||||
}
|
||||
@@ -23,7 +24,8 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
return await readDatabaseOfferWithARM(params.databaseId);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { FabricArtifactInfo, userContext } from "../../UserContext";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
|
||||
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
|
||||
@@ -14,8 +15,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
let databases: DataModels.Database[];
|
||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||
if (
|
||||
isFabricMirroredKey() &&
|
||||
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
|
||||
.resourceTokens
|
||||
) {
|
||||
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
|
||||
.resourceTokenInfo;
|
||||
|
||||
const databaseIdsSet = new Set<string>(); // databaseId
|
||||
|
||||
@@ -46,13 +52,28 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||
}));
|
||||
clearMessage();
|
||||
return databases;
|
||||
} else if (isFabricNative() && userContext.fabricContext?.databaseName) {
|
||||
const databaseId = userContext.fabricContext.databaseName;
|
||||
databases = [
|
||||
{
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: databaseId,
|
||||
collections: [],
|
||||
},
|
||||
];
|
||||
clearMessage();
|
||||
return databases;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
databases = await readDatabasesWithARM();
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Collection } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -36,7 +37,8 @@ export async function updateCollection(
|
||||
if (
|
||||
userContext.authType === AuthType.AAD &&
|
||||
!userContext.features.enableSDKoperations &&
|
||||
userContext.apiType !== "Tables"
|
||||
userContext.apiType !== "Tables" &&
|
||||
!isFabric()
|
||||
) {
|
||||
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OfferDefinition, RequestOptions } from "@azure/cosmos";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import {
|
||||
migrateCassandraKeyspaceToAutoscale,
|
||||
@@ -56,7 +57,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
|
||||
|
||||
try {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
|
||||
if (params.collectionId) {
|
||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||
} else if (userContext.apiType === "Tables") {
|
||||
@@ -359,6 +360,13 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
||||
body.properties.resource.throughput = params.manualThroughput;
|
||||
}
|
||||
|
||||
if (params.throughputBuckets) {
|
||||
const throughputBuckets = params.throughputBuckets.filter(
|
||||
(bucket: ThroughputBucket) => bucket.maxThroughputPercentage !== 100,
|
||||
);
|
||||
body.properties.resource.throughputBuckets = throughputBuckets;
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
@@ -50,10 +49,8 @@ export interface ConfigContext {
|
||||
CATALOG_API_KEY: string;
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
BACKEND_ENDPOINT?: string;
|
||||
PORTAL_BACKEND_ENDPOINT: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_BACKEND_ENDPOINT?: string;
|
||||
MONGO_PROXY_ENDPOINT: string;
|
||||
CASSANDRA_PROXY_ENDPOINT: string;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
@@ -109,7 +106,6 @@ let configContext: Readonly<ConfigContext> = {
|
||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||
JUNO_ENDPOINT: JunoEndpoints.Prod,
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
@@ -152,15 +148,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.BACKEND_ENDPOINT,
|
||||
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||
)
|
||||
) {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.MONGO_PROXY_ENDPOINT,
|
||||
@@ -170,10 +157,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.MONGO_PROXY_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.CASSANDRA_PROXY_ENDPOINT,
|
||||
|
||||
@@ -275,6 +275,12 @@ export interface Offer {
|
||||
offerReplacePending: boolean;
|
||||
instantMaximumThroughput?: number;
|
||||
softAllowedMaximumThroughput?: number;
|
||||
throughputBuckets?: ThroughputBucket[];
|
||||
}
|
||||
|
||||
export interface ThroughputBucket {
|
||||
id: number;
|
||||
maxThroughputPercentage: number;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
@@ -397,6 +403,7 @@ export interface UpdateOfferParams {
|
||||
collectionId?: string;
|
||||
migrateToAutoPilot?: boolean;
|
||||
migrateToManual?: boolean;
|
||||
throughputBuckets?: ThroughputBucket[];
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
export enum FabricMessageTypes {
|
||||
GetAuthorizationToken = "GetAuthorizationToken",
|
||||
GetAllResourceTokens = "GetAllResourceTokens",
|
||||
GetAccessToken = "GetAccessToken",
|
||||
Ready = "Ready",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,9 @@
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { AuthorizationToken } from "./FabricMessageTypes";
|
||||
|
||||
// This is the version of these messages
|
||||
export const FABRIC_RPC_VERSION = "2";
|
||||
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
||||
|
||||
// Fabric to Data Explorer
|
||||
|
||||
// TODO Deprecated. Remove this section once DE is updated
|
||||
export type FabricMessageV1 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
message: {
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
error: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "allResourceTokens";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
endpoint: string | undefined;
|
||||
databaseId: string | undefined;
|
||||
resourceTokens: unknown | undefined;
|
||||
resourceTokensTimestamp: number | undefined;
|
||||
};
|
||||
};
|
||||
// -----------------------------
|
||||
|
||||
export type FabricMessageV2 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
@@ -69,7 +31,7 @@ export type FabricMessageV2 =
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: FabricDatabaseConnectionInfo | undefined;
|
||||
data: ResourceTokenInfo | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -79,17 +41,81 @@ export type FabricMessageV2 =
|
||||
};
|
||||
};
|
||||
|
||||
export type CosmosDBTokenResponse = {
|
||||
token: string;
|
||||
date: string;
|
||||
};
|
||||
export type FabricMessageV3 =
|
||||
| {
|
||||
type: "newContainer";
|
||||
databaseName: string;
|
||||
}
|
||||
| {
|
||||
type: "initialize";
|
||||
version: string;
|
||||
id: string;
|
||||
message: InitializeMessageV3<CosmosDbArtifactType>;
|
||||
}
|
||||
| {
|
||||
type: "authorizationToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: AuthorizationToken | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "allResourceTokens_v2";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: ResourceTokenInfo | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "explorerVisible";
|
||||
message: {
|
||||
visible: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "accessToken";
|
||||
message: {
|
||||
id: string;
|
||||
error: string | undefined;
|
||||
data: { accessToken: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type CosmosDBConnectionInfoResponse = {
|
||||
export enum CosmosDbArtifactType {
|
||||
MIRRORED_KEY = "MIRRORED_KEY",
|
||||
MIRRORED_AAD = "MIRRORED_AAD",
|
||||
NATIVE = "NATIVE",
|
||||
}
|
||||
export interface ArtifactConnectionInfo {
|
||||
[CosmosDbArtifactType.MIRRORED_KEY]: { connectionId: string };
|
||||
[CosmosDbArtifactType.MIRRORED_AAD]: AccessTokenConnectionInfo;
|
||||
[CosmosDbArtifactType.NATIVE]: AccessTokenConnectionInfo;
|
||||
}
|
||||
|
||||
export interface AccessTokenConnectionInfo {
|
||||
accessToken: string;
|
||||
databaseName: string;
|
||||
accountEndpoint: string;
|
||||
}
|
||||
|
||||
export interface InitializeMessageV3<T extends CosmosDbArtifactType> {
|
||||
connectionId: string;
|
||||
isVisible: boolean;
|
||||
isReadOnly: boolean;
|
||||
artifactType: T;
|
||||
artifactConnectionInfo: ArtifactConnectionInfo[T];
|
||||
}
|
||||
export interface CosmosDBConnectionInfoResponse {
|
||||
endpoint: string;
|
||||
databaseId: string;
|
||||
resourceTokens: { [resourceId: string]: string };
|
||||
};
|
||||
resourceTokens: Record<string, string> | undefined;
|
||||
accessToken: string | undefined;
|
||||
isReadOnly: boolean;
|
||||
credentialType: "Key" | "OAuth2" | undefined;
|
||||
}
|
||||
|
||||
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
||||
resourceTokensTimestamp: number;
|
||||
}
|
||||
|
||||
@@ -406,7 +406,6 @@ export interface DataExplorerInputsFrame {
|
||||
csmEndpoint?: string;
|
||||
dnsSuffix?: string;
|
||||
serverId?: string;
|
||||
extensionEndpoint?: string;
|
||||
portalBackendEndpoint?: string;
|
||||
mongoProxyEndpoint?: string;
|
||||
cassandraProxyEndpoint?: string;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@@ -19,7 +21,6 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
|
||||
@@ -41,7 +42,7 @@ export interface DatabaseContextMenuButtonParams {
|
||||
* New resource tree (in ReactJS)
|
||||
*/
|
||||
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
@@ -145,7 +146,7 @@ export const createCollectionContextMenuButton = (
|
||||
});
|
||||
}
|
||||
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
||||
items.push({
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AuthType } from "AuthType";
|
||||
import { shallow } from "enzyme";
|
||||
import ko from "knockout";
|
||||
import { Features } from "Platform/Hosted/extractFeatures";
|
||||
import React from "react";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
@@ -247,4 +249,42 @@ describe("SettingsComponent", () => {
|
||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||
});
|
||||
|
||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
features: { enableThroughputBuckets: true } as Features,
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
const isEnabled = settingsComponentInstance["throughputBucketsEnabled"];
|
||||
expect(isEnabled).toBe(true);
|
||||
|
||||
wrapper.setState({
|
||||
isThroughputBucketsSaveable: true,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
await settingsComponentInstance.onSaveClick();
|
||||
|
||||
expect(updateOffer).toHaveBeenCalledWith({
|
||||
databaseId: collection.databaseId,
|
||||
collectionId: collection.id(),
|
||||
currentOffer: expect.any(Object),
|
||||
autopilotThroughput: collection.offer().autoscaleMaxThroughput,
|
||||
manualThroughput: collection.offer().manualThroughput,
|
||||
throughputBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(wrapper.state("isThroughputBucketsSaveable")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
ContainerPolicyComponent,
|
||||
ContainerPolicyComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
|
||||
import {
|
||||
ThroughputBucketsComponent,
|
||||
ThroughputBucketsComponentProps,
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
@@ -86,6 +90,8 @@ export interface SettingsComponentState {
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
isScaleSaveable: boolean;
|
||||
isScaleDiscardable: boolean;
|
||||
throughputBuckets: DataModels.ThroughputBucket[];
|
||||
throughputBucketsBaseline: DataModels.ThroughputBucket[];
|
||||
throughputError: string;
|
||||
|
||||
timeToLive: TtlType;
|
||||
@@ -104,6 +110,7 @@ export interface SettingsComponentState {
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
isSubSettingsSaveable: boolean;
|
||||
isSubSettingsDiscardable: boolean;
|
||||
isThroughputBucketsSaveable: boolean;
|
||||
|
||||
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
|
||||
@@ -158,6 +165,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private isVectorSearchEnabled: boolean;
|
||||
private isFullTextSearchEnabled: boolean;
|
||||
private totalThroughputUsed: number;
|
||||
private throughputBucketsEnabled: boolean;
|
||||
public mongoDBCollectionResource: MongoDBCollectionResource;
|
||||
|
||||
constructor(props: SettingsComponentProps) {
|
||||
@@ -175,6 +183,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
this.throughputBucketsEnabled =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.features.enableThroughputBuckets &&
|
||||
userContext.authType === AuthType.AAD;
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
@@ -193,6 +205,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
wasAutopilotOriginallySet: false,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughputBuckets: undefined,
|
||||
throughputBucketsBaseline: undefined,
|
||||
throughputError: undefined,
|
||||
|
||||
timeToLive: undefined,
|
||||
@@ -211,6 +225,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
changeFeedPolicyBaseline: undefined,
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
|
||||
vectorEmbeddingPolicy: undefined,
|
||||
vectorEmbeddingPolicyBaseline: undefined,
|
||||
@@ -327,7 +342,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
);
|
||||
};
|
||||
|
||||
@@ -339,7 +355,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty ||
|
||||
this.state.isComputedPropertiesDirty ||
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
|
||||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
|
||||
this.state.isThroughputBucketsSaveable
|
||||
);
|
||||
};
|
||||
|
||||
@@ -419,6 +436,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
this.setState({
|
||||
throughput: this.state.throughputBaseline,
|
||||
throughputBuckets: this.state.throughputBucketsBaseline,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
timeToLive: this.state.timeToLiveBaseline,
|
||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
|
||||
@@ -441,6 +460,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: false,
|
||||
isThroughputBucketsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isContainerPolicyDirty: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
@@ -479,6 +499,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||
|
||||
private onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
|
||||
this.setState({ isThroughputBucketsSaveable: isSaveable });
|
||||
};
|
||||
|
||||
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
|
||||
|
||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||
@@ -749,9 +773,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
] as DataModels.ComputedProperties;
|
||||
}
|
||||
|
||||
const throughputBuckets = this.offer?.throughputBuckets;
|
||||
|
||||
return {
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
throughputBuckets,
|
||||
throughputBucketsBaseline: throughputBuckets,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||
timeToLive: timeToLive,
|
||||
@@ -839,6 +867,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({ throughput: newThroughput, throughputError });
|
||||
};
|
||||
|
||||
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
|
||||
this.setState({ throughputBuckets });
|
||||
};
|
||||
|
||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||
|
||||
@@ -1029,6 +1061,24 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled && this.state.isThroughputBucketsSaveable) {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer({
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
||||
? this.collection.offer().autoscaleMaxThroughput
|
||||
: undefined,
|
||||
manualThroughput: this.collection.offer().manualThroughput
|
||||
? this.collection.offer().manualThroughput
|
||||
: undefined,
|
||||
throughputBuckets: this.state.throughputBuckets,
|
||||
});
|
||||
this.collection.offer(updatedOffer);
|
||||
this.offer = updatedOffer;
|
||||
this.setState({ isThroughputBucketsSaveable: false });
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
@@ -1209,6 +1259,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||
};
|
||||
|
||||
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
|
||||
currentBuckets: this.state.throughputBuckets,
|
||||
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
|
||||
onBucketsChange: this.onThroughputBucketChange,
|
||||
onSaveableChange: this.onThroughputBucketsSaveableChange,
|
||||
};
|
||||
|
||||
const partitionKeyComponentProps: PartitionKeyComponentProps = {
|
||||
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
|
||||
collection: this.collection,
|
||||
@@ -1271,6 +1328,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { ThroughputBucketsComponent } from "./ThroughputBucketsComponent";
|
||||
|
||||
describe("ThroughputBucketsComponent", () => {
|
||||
const mockOnBucketsChange = jest.fn();
|
||||
const mockOnSaveableChange = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
currentBuckets: [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
],
|
||||
throughputBucketsBaseline: [
|
||||
{ id: 1, maxThroughputPercentage: 40 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
],
|
||||
onBucketsChange: mockOnBucketsChange,
|
||||
onSaveableChange: mockOnSaveableChange,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the correct number of buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("renders buckets in the correct order even if input is unordered", () => {
|
||||
const unorderedBuckets = [
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
];
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
||||
|
||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
||||
});
|
||||
|
||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
||||
const oversizedBuckets = [
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 70 },
|
||||
{ id: 4, maxThroughputPercentage: 80 },
|
||||
{ id: 5, maxThroughputPercentage: 90 },
|
||||
{ id: 6, maxThroughputPercentage: 100 },
|
||||
{ id: 7, maxThroughputPercentage: 40 },
|
||||
];
|
||||
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
||||
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
||||
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("70")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("80")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("90")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("100")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onBucketsChange when a bucket value changes", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "70" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("triggers onSaveableChange when values change", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnSaveableChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("updates state consistently after multiple changes to different buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
const input2 = screen.getByDisplayValue("60");
|
||||
fireEvent.change(input2, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 80 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("resets to baseline when currentBuckets are reset", () => {
|
||||
const { rerender } = render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input1 = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input1, { target: { value: "70" } });
|
||||
|
||||
rerender(<ThroughputBucketsComponent {...defaultProps} currentBuckets={defaultProps.throughputBucketsBaseline} />);
|
||||
|
||||
expect(screen.getByDisplayValue("40")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not call onBucketsChange when value remains unchanged", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const input = screen.getByDisplayValue("50");
|
||||
fireEvent.change(input, { target: { value: "50" } });
|
||||
|
||||
expect(mockOnBucketsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables input and slider when maxThroughputPercentage is 100", () => {
|
||||
render(
|
||||
<ThroughputBucketsComponent
|
||||
{...defaultProps}
|
||||
currentBuckets={[
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const disabledInputs = screen.getAllByDisplayValue("100");
|
||||
expect(disabledInputs.length).toBeGreaterThan(0);
|
||||
expect(disabledInputs[0]).toBeDisabled();
|
||||
|
||||
const sliders = screen.getAllByRole("slider");
|
||||
expect(sliders.length).toBeGreaterThan(0);
|
||||
expect(sliders[0]).toHaveAttribute("aria-disabled", "true");
|
||||
expect(sliders[1]).toHaveAttribute("aria-disabled", "false");
|
||||
});
|
||||
|
||||
it("toggles bucket value between 50 and 100 with switch", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
const toggles = screen.getAllByRole("switch");
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("ensures default buckets are used when no buckets are provided", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
||||
import { ThroughputBucket } from "Contracts/DataModels";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { isDirty } from "../../SettingsUtils";
|
||||
|
||||
const MAX_BUCKET_SIZES = 5;
|
||||
|
||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
export interface ThroughputBucketsComponentProps {
|
||||
currentBuckets: ThroughputBucket[];
|
||||
throughputBucketsBaseline: ThroughputBucket[];
|
||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
||||
onSaveableChange: (isSaveable: boolean) => void;
|
||||
}
|
||||
|
||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
||||
currentBuckets,
|
||||
throughputBucketsBaseline,
|
||||
onBucketsChange,
|
||||
onSaveableChange,
|
||||
}) => {
|
||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
||||
if (!buckets || buckets.length === 0) {
|
||||
return DEFAULT_BUCKETS;
|
||||
}
|
||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
return adjustedDefaultBuckets.map(
|
||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
||||
);
|
||||
};
|
||||
|
||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
||||
|
||||
useEffect(() => {
|
||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
||||
onSaveableChange(false);
|
||||
}, [currentBuckets]);
|
||||
|
||||
useEffect(() => {
|
||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
||||
onSaveableChange(isChanged);
|
||||
}, [throughputBuckets]);
|
||||
|
||||
const handleBucketChange = (id: number, newValue: number) => {
|
||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
||||
);
|
||||
setThroughputBuckets(updatedBuckets);
|
||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||
settingsChanged && onBucketsChange(updatedBuckets);
|
||||
};
|
||||
|
||||
const onToggle = (id: number, checked: boolean) => {
|
||||
handleBucketChange(id, checked ? 50 : 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
||||
<Label>Throughput Buckets</Label>
|
||||
<Stack>
|
||||
{throughputBuckets?.map((bucket) => (
|
||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
||||
<Slider
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
showValue={false}
|
||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<TextField
|
||||
value={bucket.maxThroughputPercentage.toString()}
|
||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
||||
type="number"
|
||||
suffix="%"
|
||||
styles={{
|
||||
fieldGroup: { width: 80 },
|
||||
}}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<Toggle
|
||||
onText="Active"
|
||||
offText="Inactive"
|
||||
checked={bucket.maxThroughputPercentage !== 100}
|
||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
||||
></Toggle>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,8 @@ export type isDirtyTypes =
|
||||
| DataModels.IndexingPolicy
|
||||
| DataModels.ComputedProperties
|
||||
| DataModels.VectorEmbedding[]
|
||||
| DataModels.FullTextPolicy;
|
||||
| DataModels.FullTextPolicy
|
||||
| DataModels.ThroughputBucket[];
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
export const TtlOnNoDefault = "on-nodefault";
|
||||
@@ -55,6 +56,7 @@ export enum SettingsV2TabTypes {
|
||||
PartitionKeyTab,
|
||||
ComputedPropertiesTab,
|
||||
ContainerVectorPolicyTab,
|
||||
ThroughputBucketsTab,
|
||||
}
|
||||
|
||||
export enum ContainerPolicyTabTypes {
|
||||
@@ -167,6 +169,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
return "Computed Properties";
|
||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||
return "Container Policies";
|
||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||
return "Throughput Buckets";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
|
||||
@@ -35,12 +35,20 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
setIsThroughputCapExceeded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
const defaultThroughput: number =
|
||||
let defaultThroughput: number;
|
||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||
|
||||
if (
|
||||
isFreeTier ||
|
||||
isQuickstart ||
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||
) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
|
||||
} else {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
|
||||
}
|
||||
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(defaultThroughput);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil";
|
||||
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||
@@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||
@@ -187,6 +187,10 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (isFabricMirrored()) {
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Home);
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
@@ -347,8 +351,8 @@ export default class Explorer {
|
||||
};
|
||||
|
||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||
if (isFabricMirroredKey()) {
|
||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1127,7 +1131,7 @@ export default class Explorer {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
|
||||
await this.refreshSampleData();
|
||||
this.refreshSampleData();
|
||||
}
|
||||
|
||||
public async configureCopilot(): Promise<void> {
|
||||
@@ -1152,26 +1156,27 @@ export default class Explorer {
|
||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||
}
|
||||
|
||||
public async refreshSampleData(): Promise<void> {
|
||||
try {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
return;
|
||||
}
|
||||
const collection: DataModels.Collection = await readSampleCollection();
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer");
|
||||
public refreshSampleData(): void {
|
||||
if (!userContext.sampleDataConnectionInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||
if (!databaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
readSampleCollection()
|
||||
.then((collection: DataModels.Collection) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
.flex-direction(@direction: row);
|
||||
padding: 4px 5px;
|
||||
|
||||
label {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.valueCol {
|
||||
flex-grow: 1;
|
||||
padding-right: 5px;
|
||||
@@ -63,6 +59,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.customTrashIcon {
|
||||
padding-top: 33px;
|
||||
}
|
||||
|
||||
.rightPaneTrashIconImg {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -142,10 +142,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
<div className="labelCol">
|
||||
<TextField
|
||||
className="edgeInput"
|
||||
label={index === 0 && "Key"}
|
||||
type="text"
|
||||
id="propertyKeyNewVertexPane"
|
||||
componentRef={input}
|
||||
aria-required="true"
|
||||
required
|
||||
placeholder="Key"
|
||||
autoComplete="off"
|
||||
aria-label={`Enter value for propery ${index + 1}`}
|
||||
@@ -153,11 +154,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||
/>
|
||||
</div>
|
||||
<span className="mandatoryStar">* </span>
|
||||
|
||||
<div className="valueCol">
|
||||
<TextField
|
||||
className="edgeInput"
|
||||
label={index === 0 && "Value"}
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
autoComplete="off"
|
||||
@@ -169,6 +170,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
<div>
|
||||
<Dropdown
|
||||
role="combobox"
|
||||
label={index === 0 && "Type"}
|
||||
ariaLabel="Type"
|
||||
placeholder="Select an option"
|
||||
defaultSelectedKey={data.values[0].type}
|
||||
style={{ width: 100 }}
|
||||
@@ -181,7 +184,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
</div>
|
||||
<div className="actionCol">
|
||||
<div
|
||||
className="rightPaneTrashIcon rightPaneBtns"
|
||||
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`Delete ${data.key}`}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { Platform, configContext } from "../../../ConfigContext";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
@@ -93,19 +93,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const rootStyle =
|
||||
configContext.platform === Platform.Fabric
|
||||
? {
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
padding: "2px 8px 0px 8px",
|
||||
},
|
||||
}
|
||||
: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
const rootStyle = isFabric()
|
||||
? {
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
padding: "2px 8px 0px 8px",
|
||||
},
|
||||
}
|
||||
: {
|
||||
root: {
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
|
||||
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);
|
||||
|
||||
@@ -37,21 +37,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Button should not be visible for Tables API", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
||||
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
||||
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
||||
//
|
||||
// it("Button should not be visible for Tables API", () => {
|
||||
// updateUserContext({
|
||||
// databaseAccount: {
|
||||
// properties: {
|
||||
// capabilities: [{ name: "EnableTable" }],
|
||||
// },
|
||||
// } as DatabaseAccount,
|
||||
// });
|
||||
//
|
||||
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
// const enableAzureSynapseLinkBtn = buttons.find(
|
||||
// (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||
// );
|
||||
// expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
//});
|
||||
|
||||
it("Button should not be visible for Cassandra API", () => {
|
||||
updateUserContext({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||
@@ -61,7 +62,7 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
}
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
&:active {
|
||||
background-color:@NotificationHigh;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.focusedBorder();
|
||||
}
|
||||
|
||||
.statusBar {
|
||||
.dataTypeIcons {
|
||||
|
||||
@@ -81,10 +81,6 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
public setElememntRef = (element: HTMLElement): void => {
|
||||
this.consoleHeaderElement = element;
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const numInProgress = this.state.allConsoleData.filter(
|
||||
(data: ConsoleData) => data.type === ConsoleDataType.InProgress,
|
||||
@@ -101,7 +97,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
ref={this.setElememntRef}
|
||||
role="button"
|
||||
aria-label="Console"
|
||||
aria-expanded={this.props.isConsoleExpanded}
|
||||
onClick={() => this.expandCollapseConsole()}
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||
tabIndex={0}
|
||||
@@ -109,15 +107,15 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div className="statusBar">
|
||||
<span className="dataTypeIcons">
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={LoadingIcon} alt="in progress items" />
|
||||
<img src={LoadingIcon} alt="In progress items" />
|
||||
<span className="numInProgress">{numInProgress}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={ErrorBlackIcon} alt="error items" />
|
||||
<img src={ErrorBlackIcon} alt="Error items" />
|
||||
<span className="numErroredItems">{numErroredItems}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={infoBubbleIcon} alt="info items" />
|
||||
<img src={infoBubbleIcon} alt="Info items" />
|
||||
<span className="numInfoItems">{numInfoItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -129,17 +127,10 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
||||
<img
|
||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,9 +250,6 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
|
||||
private onConsoleWasExpanded = (): void => {
|
||||
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||
this.consoleHeaderElement.focus();
|
||||
}
|
||||
useNotificationConsole.getState().setConsoleAnimationFinished(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleContainer"
|
||||
>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
@@ -21,7 +24,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
alt="In progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -34,7 +37,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
alt="Error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -47,7 +50,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
alt="Info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -71,15 +74,11 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="ChevronUpIcon"
|
||||
alt="Expand icon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
@@ -176,10 +175,13 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleContainer"
|
||||
>
|
||||
<div
|
||||
aria-expanded={false}
|
||||
aria-label="Console"
|
||||
className="notificationConsoleHeader"
|
||||
id="notificationConsoleHeader"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
@@ -192,7 +194,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="in progress items"
|
||||
alt="In progress items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -205,7 +207,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="error items"
|
||||
alt="Error items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -218,7 +220,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="info items"
|
||||
alt="Info items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
@@ -244,15 +246,11 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
data-test="NotificationConsole/ExpandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt="ChevronUpIcon"
|
||||
alt="Expand icon"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
|
||||
@@ -19,7 +19,7 @@ export class NotebookContainerClient {
|
||||
private clearReconnectionAttemptMessage? = () => {};
|
||||
private isResettingWorkspace: boolean;
|
||||
private phoenixClient: PhoenixClient;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private retryOptions: Options;
|
||||
private scheduleTimerId: NodeJS.Timeout;
|
||||
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
@@ -128,9 +127,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations)
|
||||
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
|
||||
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
const response = await fetch(disallowedLocationsUri, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { ActionContracts } from "../../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -58,9 +58,9 @@ function openCollectionTab(
|
||||
}
|
||||
|
||||
if (
|
||||
configContext.platform === Platform.Fabric &&
|
||||
isFabricMirrored() &&
|
||||
!(
|
||||
// whitelist the tab kinds that are allowed to be opened in Fabric
|
||||
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored
|
||||
(
|
||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
isVectorSearchEnabled,
|
||||
} from "Utils/CapabilityUtils";
|
||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
import "../Controls/ThroughputInput/ThroughputInput.less";
|
||||
@@ -284,150 +286,152 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
|
||||
<div className="panelMainContent">
|
||||
<Stack hidden={userContext.apiType === "Tables"}>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Database {userContext.apiType === "Mongo" ? "name" : "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}
|
||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
||||
<Stack hidden={userContext.apiType === "Tables"}>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Database {userContext.apiType === "Mongo" ? "name" : "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()}.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
{configContext.platform !== Platform.Fabric && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</div>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.state.createNewDatabase && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<input
|
||||
name="newDatabaseId"
|
||||
id="newDatabaseId"
|
||||
aria-required
|
||||
required
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label="New database id, Type a new database id"
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ newDatabaseId: event.target.value })
|
||||
}
|
||||
/>
|
||||
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||
checked={this.state.isSharedThroughputChecked}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ isSharedThroughputChecked: isChecked })
|
||||
}
|
||||
{configContext.platform !== Platform.Fabric && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.state.createNewDatabase && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<input
|
||||
name="newDatabaseId"
|
||||
id="newDatabaseId"
|
||||
aria-required
|
||||
required
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label="New database id, Type a new database id"
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ newDatabaseId: event.target.value })
|
||||
}
|
||||
/>
|
||||
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||
checked={this.state.isSharedThroughputChecked}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ isSharedThroughputChecked: isChecked })
|
||||
}
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
this.setState({ isThroughputCapExceeded })
|
||||
}
|
||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{!this.state.createNewDatabase && (
|
||||
<Dropdown
|
||||
ariaLabel="Choose an existing database"
|
||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||
style={{ width: 300, fontSize: 12 }}
|
||||
placeholder="Choose an existing database"
|
||||
options={this.getDatabaseOptions()}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
||||
this.setState({ selectedDatabaseId: database.key as string })
|
||||
}
|
||||
defaultSelectedKey={this.props.databaseId}
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" />
|
||||
</Stack>
|
||||
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
isQuickstart={this.props.isQuickstart}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
this.setState({ isThroughputCapExceeded })
|
||||
}
|
||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{!this.state.createNewDatabase && (
|
||||
<Dropdown
|
||||
ariaLabel="Choose an existing database"
|
||||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||||
style={{ width: 300, fontSize: 12 }}
|
||||
placeholder="Choose an existing database"
|
||||
options={this.getDatabaseOptions()}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
||||
this.setState({ selectedDatabaseId: database.key as string })
|
||||
}
|
||||
defaultSelectedKey={this.props.databaseId}
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
@@ -456,8 +460,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
@@ -666,7 +670,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType === "SQL" && (
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
@@ -747,7 +751,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
@@ -937,7 +941,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
)}
|
||||
{userContext.apiType !== "Tables" && (
|
||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
@@ -1260,7 +1264,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
// }
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isServerlessAccount()) {
|
||||
if (isFabricNative() || isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1286,7 +1290,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private shouldShowAnalyticalStoreOptions(): boolean {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
if (isFabricNative() || configContext.platform === Platform.Emulator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
@@ -204,8 +205,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
type="text"
|
||||
aria-required="true"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
size={40}
|
||||
aria-label={databaseIdLabel}
|
||||
placeholder={databaseIdPlaceHolder}
|
||||
|
||||
@@ -39,7 +39,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
|
||||
data-lpignore={true}
|
||||
id="database-id"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
styles={
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
@@ -202,8 +203,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
styles={getTextFieldStyles()}
|
||||
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Type a new keyspace id"
|
||||
size={40}
|
||||
value={newKeyspaceId}
|
||||
@@ -292,8 +293,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
required={true}
|
||||
ariaLabel="addCollection-table Id Create table"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\-]*[^/?#- \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?' '-'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter table Id"
|
||||
size={20}
|
||||
value={tableId}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -235,8 +236,8 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
padding-left: @MediumSpace;
|
||||
|
||||
.paneErrorLink {
|
||||
color: @LinkColor;
|
||||
cursor: pointer;
|
||||
font-size: @mediumFontSize;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "Shared/StorageUtility";
|
||||
import * as StringUtility from "Shared/StringUtility";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
|
||||
@@ -183,7 +184,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const showEnableEntraIdRbac =
|
||||
userContext.apiType === "SQL" &&
|
||||
isDataplaneRbacSupported(userContext.apiType) &&
|
||||
userContext.authType === AuthType.AAD &&
|
||||
configContext.platform !== Platform.Fabric &&
|
||||
!isEmulator;
|
||||
|
||||
@@ -93,7 +93,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
id="newDatabaseId"
|
||||
name="newDatabaseId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
||||
placeholder="Type a new database id"
|
||||
required={true}
|
||||
size={40}
|
||||
@@ -178,7 +178,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
id="collectionId"
|
||||
name="collectionId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
pattern="[^\\/?#\\\\]*[^\\/?# \\\\]"
|
||||
placeholder="e.g., Container1"
|
||||
required={true}
|
||||
size={40}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
|
||||
import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import QueryError, { QueryErrorSeverity } from "Common/QueryError";
|
||||
import { createUri } from "Common/UrlUtility";
|
||||
@@ -393,8 +393,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
},
|
||||
}}
|
||||
disabled={isGeneratingQuery}
|
||||
autoComplete="list"
|
||||
aria-expanded={showSamplePrompts}
|
||||
autoComplete="off"
|
||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||
aria-labelledby="copilot-textfield-label"
|
||||
onRenderSuffix={() => {
|
||||
@@ -580,7 +579,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
|
||||
{userContext.feedbackPolicies?.policyAllowFeedback && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
|
||||
<Text style={{ fontSize: 12 }}>{FeedbackLabels.provideFeedback}</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
role="status"
|
||||
@@ -630,8 +629,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 10 }}
|
||||
aria-label="Like"
|
||||
role="toggle"
|
||||
aria-label={FeedbackLabels.provideFeedback}
|
||||
role="button"
|
||||
title="Like"
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setShowCallout(!likeQuery);
|
||||
@@ -649,8 +649,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 4px" }}
|
||||
role="toggle"
|
||||
aria-label="Dislike"
|
||||
role="button"
|
||||
aria-label={FeedbackLabels.provideFeedback}
|
||||
title="Dislike"
|
||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
let toggleStatusValue = "Unpressed";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Areas,
|
||||
BackendApi,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpStatusCodes,
|
||||
@@ -32,7 +31,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -82,9 +80,7 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
||||
};
|
||||
|
||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
const backendEndpoint: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
|
||||
const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
|
||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Button,
|
||||
makeStyles,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuButtonProps,
|
||||
@@ -7,13 +8,12 @@ import {
|
||||
MenuList,
|
||||
MenuPopover,
|
||||
MenuTrigger,
|
||||
SplitButton,
|
||||
makeStyles,
|
||||
mergeClasses,
|
||||
shorthands,
|
||||
SplitButton,
|
||||
} from "@fluentui/react-components";
|
||||
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
|
||||
import { Tabs } from "Explorer/Tabs/Tabs";
|
||||
@@ -21,6 +21,7 @@ import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/T
|
||||
import { ResourceTree } from "Explorer/Tree/ResourceTree";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
||||
import { Allotment, AllotmentHandle } from "allotment";
|
||||
@@ -123,7 +124,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
|
||||
const actions = useMemo<GlobalCommand[]>(() => {
|
||||
if (
|
||||
configContext.platform === Platform.Fabric ||
|
||||
(isFabric() && userContext.fabricContext?.isReadOnly) ||
|
||||
userContext.apiType === "Postgres" ||
|
||||
userContext.apiType === "VCoreMongo"
|
||||
) {
|
||||
@@ -137,12 +138,15 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
||||
id: "new_collection",
|
||||
label: `New ${getCollectionName()}`,
|
||||
icon: <Add16Regular />,
|
||||
onClick: () => explorer.onNewCollectionClicked(),
|
||||
onClick: () => {
|
||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
||||
explorer.onNewCollectionClicked({ databaseId });
|
||||
},
|
||||
keyboardAction: KeyboardAction.NEW_COLLECTION,
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType !== "Tables") {
|
||||
if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") {
|
||||
actions.push({
|
||||
id: "new_database",
|
||||
label: `New ${getDatabaseName()}`,
|
||||
@@ -288,7 +292,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
}, [setLoading]);
|
||||
|
||||
const hasGlobalCommands = !(
|
||||
configContext.platform === Platform.Fabric ||
|
||||
isFabricMirrored() ||
|
||||
userContext.apiType === "Postgres" ||
|
||||
userContext.apiType === "VCoreMongo"
|
||||
);
|
||||
|
||||
173
src/Explorer/SplashScreen/FabricHome.tsx
Normal file
173
src/Explorer/SplashScreen/FabricHome.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
export interface SplashScreenProps {
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
homeContainer: {
|
||||
width: "100%",
|
||||
alignContent: "center",
|
||||
},
|
||||
title: {
|
||||
textAlign: "center",
|
||||
fontSize: "20px",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
buttonsContainer: {
|
||||
width: "584px",
|
||||
margin: "auto",
|
||||
display: "grid",
|
||||
padding: "16px",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gap: "10px",
|
||||
gridAutoRows: "minmax(184px, auto)",
|
||||
},
|
||||
one: {
|
||||
gridColumn: "1 / 3",
|
||||
gridRow: "1 / 3",
|
||||
"& svg": {
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
two: {
|
||||
gridColumn: "3",
|
||||
gridRow: "1",
|
||||
"& img": {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
three: {
|
||||
gridColumn: "3",
|
||||
gridRow: "2",
|
||||
"& svg": {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
margin: "auto",
|
||||
},
|
||||
},
|
||||
buttonContainer: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
border: "1px solid #e0e0e0",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
||||
"border-color": tokens.colorNeutralStroke1Hover,
|
||||
},
|
||||
},
|
||||
buttonUpperPart: {
|
||||
textAlign: "center",
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
backgroundColor: "#e3f7ef",
|
||||
},
|
||||
buttonLowerPart: {
|
||||
borderTop: "1px solid #e0e0e0",
|
||||
height: "76px",
|
||||
padding: "8px",
|
||||
"> div:nth-child(1)": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
},
|
||||
footer: {
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
|
||||
interface FabricHomeScreenButtonProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: JSX.Element;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className: string }> = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
className,
|
||||
onClick,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
||||
// TODO Make this a11y copmliant: aria-label for icon
|
||||
return (
|
||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
||||
<div className={styles.buttonUpperPart}>{icon}</div>
|
||||
<div className={styles.buttonLowerPart}>
|
||||
<div>{title}</div>
|
||||
<div>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
||||
const styles = useStyles();
|
||||
const getSplashScreenButtons = (): JSX.Element => {
|
||||
const buttons: FabricHomeScreenButtonProps[] = [
|
||||
{
|
||||
title: "New container",
|
||||
description: "Create a destination container to store your data",
|
||||
icon: <DocumentAddRegular />,
|
||||
onClick: () => {
|
||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
||||
props.explorer.onNewCollectionClicked({ databaseId });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Sample data",
|
||||
description: "Automatically load sample data in your database",
|
||||
icon: <img src={CosmosDbBlackIcon} />,
|
||||
},
|
||||
{
|
||||
title: "App development",
|
||||
description: "Start here to use an SDK to build your apps",
|
||||
icon: <LinkMultipleRegular />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.buttonsContainer}>
|
||||
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
|
||||
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
|
||||
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const title = "Build your database";
|
||||
return (
|
||||
<div className={styles.homeContainer}>
|
||||
<div className={styles.title} role="heading" aria-label={title}>
|
||||
{title}
|
||||
</div>
|
||||
{getSplashScreenButtons()}
|
||||
<div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
<Link href="https://cosmos.azure.com/docs" target="_blank">
|
||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants";
|
||||
import { CassandraProxyAPIs } from "../../Common/Constants";
|
||||
import { handleError } from "../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
@@ -264,9 +264,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
if (!this.useCassandraProxyEndpoint("postQuery")) {
|
||||
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
|
||||
}
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
@@ -309,55 +306,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async queryDocuments_ToBeDeprecated(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string,
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestQueryApi
|
||||
: Constants.CassandraBackend.queryApi;
|
||||
const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
query,
|
||||
paginationToken,
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
});
|
||||
shouldNotify &&
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`,
|
||||
);
|
||||
return {
|
||||
Results: data.result,
|
||||
ContinuationToken: data.paginationToken,
|
||||
};
|
||||
} catch (error) {
|
||||
shouldNotify &&
|
||||
handleError(
|
||||
error,
|
||||
"QueryDocuments_ToBeDeprecated_Cassandra",
|
||||
`Failed to query rows for table ${collection.id()}`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage?.();
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[],
|
||||
@@ -471,10 +419,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!this.useCassandraProxyEndpoint("getKeys")) {
|
||||
return this.getTableKeys_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
@@ -515,52 +459,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching keys for table ${collection.id()}`);
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestKeysApi
|
||||
: Constants.CassandraBackend.keysApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKeys>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: CassandraTableKeys) => {
|
||||
collection.cassandraKeys = data;
|
||||
logConsoleInfo(`Successfully fetched keys for table ${collection.id()}`);
|
||||
deferred.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!this.useCassandraProxyEndpoint("getSchema")) {
|
||||
return this.getTableSchema_ToBeDeprecated(collection);
|
||||
}
|
||||
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
@@ -602,52 +501,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
}
|
||||
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
|
||||
const { databaseAccount, authType } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestSchemaApi
|
||||
: Constants.CassandraBackend.schemaApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKey[]>();
|
||||
|
||||
$.ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(databaseAccount?.properties.cassandraEndpoint),
|
||||
resourceId: databaseAccount?.id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
collection.cassandraSchema = data.columns;
|
||||
logConsoleInfo(`Successfully fetched schema for table ${collection.id()}`);
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
(error: any) => {
|
||||
const errorText = error.responseJSON?.message ?? JSON.stringify(error);
|
||||
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(errorText);
|
||||
},
|
||||
)
|
||||
.done(clearInProgressMessage);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
||||
if (!this.useCassandraProxyEndpoint("createOrDelete")) {
|
||||
return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query);
|
||||
}
|
||||
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
@@ -677,38 +531,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private createOrDeleteQuery_ToBeDeprecated(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
query: string,
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer();
|
||||
const { authType, databaseAccount } = userContext;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||
: Constants.CassandraBackend.createOrDeleteApi;
|
||||
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName: databaseAccount?.name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||
resourceId: resourceId,
|
||||
query: query,
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader as any,
|
||||
cache: false,
|
||||
}).then(
|
||||
(data: any) => {
|
||||
deferred.resolve();
|
||||
},
|
||||
(reason) => {
|
||||
deferred.reject(reason);
|
||||
},
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
||||
if (!cassandraEndpoint) {
|
||||
return cassandraEndpoint;
|
||||
@@ -747,23 +569,4 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||
return collection.cassandraKeys.partitionKeys[0].property;
|
||||
}
|
||||
|
||||
private useCassandraProxyEndpoint(api: string): boolean {
|
||||
const activeCassandraProxyEndpoints: string[] = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
CassandraProxyEndpoints.Prod,
|
||||
CassandraProxyEndpoints.Fairfax,
|
||||
CassandraProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
if (configContext.globallyEnabledCassandraAPIs.includes(api)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
|
||||
activeCassandraProxyEndpoints.includes(configContext.CASSANDRA_PROXY_ENDPOINT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { deleteDocuments } from "Common/dataAccess/deleteDocument";
|
||||
import { Platform, updateConfigContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||
@@ -341,10 +342,15 @@ describe("Documents tab (noSql API)", () => {
|
||||
updateConfigContext({ platform: Platform.Fabric });
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
connectionId: "test",
|
||||
databaseConnectionInfo: undefined,
|
||||
databaseName: "database",
|
||||
artifactInfo: {
|
||||
connectionId: "test",
|
||||
resourceTokenInfo: undefined,
|
||||
},
|
||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
||||
isReadOnly: true,
|
||||
isVisible: true,
|
||||
fabricClientRpcVersion: "rpcVersion",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||
import { readDocument } from "Common/dataAccess/readDocument";
|
||||
import { updateDocument } from "Common/dataAccess/updateDocument";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
@@ -43,6 +42,7 @@ import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@@ -55,6 +55,7 @@ import DeleteDocumentIcon from "../../../../images/DeleteDocument.svg";
|
||||
import NewDocumentIcon from "../../../../images/NewDocument.svg";
|
||||
import UploadIcon from "../../../../images/Upload_16x16.svg";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||
@@ -131,6 +132,14 @@ export const useDocumentsTabStyles = makeStyles({
|
||||
backgroundColor: "white",
|
||||
zIndex: 1,
|
||||
},
|
||||
refreshBtn: {
|
||||
position: "absolute",
|
||||
top: "3px",
|
||||
right: "4px",
|
||||
float: "right",
|
||||
zIndex: 1,
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
deleteProgressContent: {
|
||||
paddingTop: tokens.spacingVerticalL,
|
||||
},
|
||||
@@ -344,7 +353,7 @@ export const getTabsButtons = ({
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
}: ButtonsDependencies): CommandButtonComponentProps[] => {
|
||||
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
// All the following buttons require write access
|
||||
return [];
|
||||
}
|
||||
@@ -1150,27 +1159,16 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
||||
}
|
||||
} else {
|
||||
if (isMongoBulkDeleteDisabled) {
|
||||
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument().
|
||||
// MongoProxyClient.deleteDocuments() should be called for all users.
|
||||
deletePromise = MongoProxyClient.deleteDocument(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
toDeleteDocumentIds[0],
|
||||
).then(() => [toDeleteDocumentIds[0]]);
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
} else {
|
||||
deletePromise = MongoProxyClient.deleteDocuments(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
toDeleteDocumentIds,
|
||||
).then(({ deletedCount, isAcknowledged }) => {
|
||||
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
|
||||
return toDeleteDocumentIds;
|
||||
}
|
||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||
});
|
||||
}
|
||||
deletePromise = MongoProxyClient.deleteDocuments(
|
||||
_collection.databaseId,
|
||||
_collection as ViewModels.Collection,
|
||||
toDeleteDocumentIds,
|
||||
).then(({ deletedCount, isAcknowledged }) => {
|
||||
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
|
||||
return toDeleteDocumentIds;
|
||||
}
|
||||
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||
});
|
||||
}
|
||||
|
||||
return deletePromise
|
||||
@@ -2054,11 +2052,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]);
|
||||
|
||||
// TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users
|
||||
// TODO: remove partitionKey.systemKey when JS SDK bug is fixed
|
||||
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete);
|
||||
const isBulkDeleteDisabled =
|
||||
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||
const isBulkDeleteDisabled = partitionKey.systemKey && !isPreferredApiMongoDB;
|
||||
// -------------------------------------------------------
|
||||
|
||||
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
||||
@@ -2150,8 +2145,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
selectedColumnIds={selectedColumnIds}
|
||||
columnDefinitions={columnDefinitions}
|
||||
isRowSelectionDisabled={
|
||||
isBulkDeleteDisabled ||
|
||||
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||
isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
|
||||
}
|
||||
onColumnSelectionChange={onColumnSelectionChange}
|
||||
defaultColumnSelection={getInitialColumnSelection()}
|
||||
@@ -2159,6 +2153,18 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
isColumnSelectionDisabled={isPreferredApiMongoDB}
|
||||
/>
|
||||
</div>
|
||||
{tableContainerSizePx?.width >= calculateOffset(selectedColumnIds.length) + 200 && (
|
||||
<div
|
||||
title="Refresh"
|
||||
className={styles.refreshBtn}
|
||||
role="button"
|
||||
onClick={() => refreshDocumentsGrid(false)}
|
||||
aria-label="Refresh"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img src={RefreshIcon} alt="Refresh" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{tableItems.length > 0 && (
|
||||
<a
|
||||
|
||||
@@ -233,7 +233,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
||||
aria-label="Select column"
|
||||
size="small"
|
||||
icon={<MoreHorizontalRegular />}
|
||||
style={{ position: "absolute", right: 0, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||
style={{ position: "absolute", right: 10, backgroundColor: tokens.colorNeutralBackground1 }}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
<MenuPopover>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
|
||||
import React, { Component } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -50,15 +48,13 @@ export default class MongoShellTabComponent extends Component<
|
||||
IMongoShellTabComponentStates
|
||||
> {
|
||||
private _logTraces: Map<string, number>;
|
||||
private _useMongoProxyEndpoint: boolean;
|
||||
|
||||
constructor(props: IMongoShellTabComponentProps) {
|
||||
super(props);
|
||||
this._logTraces = new Map();
|
||||
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
|
||||
|
||||
this.state = {
|
||||
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||
url: getMongoShellUrl(),
|
||||
};
|
||||
|
||||
props.onMongoShellTabAccessor({
|
||||
@@ -113,17 +109,9 @@ export default class MongoShellTabComponent extends Component<
|
||||
const resourceId = databaseAccount?.id;
|
||||
const accountName = databaseAccount?.name;
|
||||
const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint;
|
||||
const mongoEndpoint =
|
||||
documentEndpoint.substr(
|
||||
Constants.MongoDBAccounts.protocol.length + 3,
|
||||
documentEndpoint.length -
|
||||
(Constants.MongoDBAccounts.protocol.length + 2 + Constants.MongoDBAccounts.defaultPort.length),
|
||||
) + Constants.MongoDBAccounts.defaultPort.toString();
|
||||
const databaseId = this.props.collection.databaseId;
|
||||
const collectionId = this.props.collection.id();
|
||||
const apiEndpoint = this._useMongoProxyEndpoint
|
||||
? configContext.MONGO_PROXY_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
const apiEndpoint = configContext.MONGO_PROXY_ENDPOINT;
|
||||
const encryptedAuthToken: string = userContext.accessToken;
|
||||
|
||||
shellIframe.contentWindow.postMessage(
|
||||
@@ -132,7 +120,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
data: {
|
||||
resourceId: resourceId,
|
||||
accountName: accountName,
|
||||
mongoEndpoint: this._useMongoProxyEndpoint ? documentEndpoint : mongoEndpoint,
|
||||
mongoEndpoint: documentEndpoint,
|
||||
authorization: authorization,
|
||||
databaseId: databaseId,
|
||||
collectionId: collectionId,
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Platform, resetConfigContext, updateConfigContext } from "../../../Conf
|
||||
import { updateUserContext, userContext } from "../../../UserContext";
|
||||
import { getMongoShellUrl } from "./getMongoShellUrl";
|
||||
|
||||
const mongoBackendEndpoint = "https://localhost:1234";
|
||||
|
||||
describe("getMongoShellUrl", () => {
|
||||
let queryString = "";
|
||||
|
||||
@@ -11,7 +9,6 @@ describe("getMongoShellUrl", () => {
|
||||
resetConfigContext();
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||
platform: Platform.Hosted,
|
||||
});
|
||||
|
||||
@@ -37,12 +34,7 @@ describe("getMongoShellUrl", () => {
|
||||
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
|
||||
});
|
||||
|
||||
it("should return /indexv2.html by default", () => {
|
||||
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
|
||||
});
|
||||
|
||||
it("should return /index.html when useMongoProxyEndpoint is true", () => {
|
||||
const useMongoProxyEndpoint: boolean = true;
|
||||
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
|
||||
it("should return /index.html by default", () => {
|
||||
expect(getMongoShellUrl().toString()).toContain(`/index.html?${queryString}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { userContext } from "../../../UserContext";
|
||||
|
||||
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
|
||||
export function getMongoShellUrl(): string {
|
||||
const { databaseAccount: account } = userContext;
|
||||
const resourceId = account?.id;
|
||||
const accountName = account?.name;
|
||||
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
|
||||
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
|
||||
|
||||
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
|
||||
return `/mongoshell/index.html?${queryString}`;
|
||||
}
|
||||
|
||||
@@ -375,6 +375,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
ruCapPerOperation: ruThreshold,
|
||||
} as QueryOperationOptions;
|
||||
}
|
||||
|
||||
const queryDocuments = async (firstItemIndex: number) =>
|
||||
await queryDocumentsPage(
|
||||
this.props.collection && this.props.collection.id(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -57,7 +58,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
}
|
||||
|
||||
this.id = editable.observable<string>();
|
||||
this.id.validations([ScriptTabBase._isValidId]);
|
||||
this.id.validations([IsValidCosmosDbResourceId]);
|
||||
|
||||
this.editorContent = editable.observable<string>();
|
||||
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
|
||||
@@ -262,29 +263,6 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
private static _isValidId(id: string): boolean {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidStartCharacters = /^[/?#\\]/;
|
||||
if (invalidStartCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||
if (invalidMiddleCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||
if (invalidEndCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _isNotEmpty(value: string): boolean {
|
||||
return !!value;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { Pivot, PivotItem } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React from "react";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -455,11 +456,12 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
public handleIdOnChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const isValidId: boolean = event.currentTarget.reportValidity();
|
||||
if (this.state.saveButton.visible) {
|
||||
this.setState({
|
||||
id: event.target.value,
|
||||
saveButton: {
|
||||
enabled: true,
|
||||
enabled: isValidId,
|
||||
visible: this.props.scriptTabBaseInstance.isNew(),
|
||||
},
|
||||
discardButton: {
|
||||
@@ -528,8 +530,8 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
className="formTree"
|
||||
type="text"
|
||||
required
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
aria-label="Stored procedure id"
|
||||
placeholder="Enter the new stored procedure id"
|
||||
size={40}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { IMessageBarStyles, MessageBar, MessageBarType } from "@fluentui/react";
|
||||
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { IpRule } from "Contracts/DataModels";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
|
||||
import { FabricHomeScreen } from "Explorer/SplashScreen/FabricHome";
|
||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
|
||||
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
|
||||
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import ko from "knockout";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
@@ -34,10 +30,6 @@ interface TabsProps {
|
||||
|
||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||
const [
|
||||
showMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
|
||||
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
|
||||
|
||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
|
||||
useEffect(() => {
|
||||
@@ -48,28 +40,8 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
});
|
||||
}, [setKeyboardHandlers]);
|
||||
|
||||
const defaultMessageBarStyles: IMessageBarStyles = {
|
||||
root: {
|
||||
height: `${LayoutConstants.rowHeight}px`,
|
||||
overflow: "hidden",
|
||||
flexDirection: "row",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
styles={defaultMessageBarStyles}
|
||||
onDismiss={() => {
|
||||
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
|
||||
}}
|
||||
>
|
||||
{`We have migrated our middleware to new infrastructure. To avoid issues with Data Explorer access, please
|
||||
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{openedReactTabs.map((tab) => (
|
||||
@@ -301,7 +273,11 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
<ConnectTab />
|
||||
);
|
||||
case ReactTabKind.Home:
|
||||
return <SplashScreen explorer={explorer} />;
|
||||
if (isFabricNative()) {
|
||||
return <FabricHomeScreen explorer={explorer} />;
|
||||
} else {
|
||||
return <SplashScreen explorer={explorer} />;
|
||||
}
|
||||
case ReactTabKind.Quickstart:
|
||||
return userContext.apiType === "VCoreMongo" ? (
|
||||
<VcoreMongoQuickstartTab explorer={explorer} />
|
||||
@@ -314,57 +290,3 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
||||
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||
}
|
||||
};
|
||||
|
||||
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
|
||||
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
|
||||
if (
|
||||
((userContext.apiType === "Mongo" && configContext.MONGO_PROXY_ENDPOINT !== MongoProxyEndpoints.Development) ||
|
||||
(userContext.apiType === "Cassandra" &&
|
||||
configContext.CASSANDRA_PROXY_ENDPOINT !== CassandraProxyEndpoints.Development)) &&
|
||||
ipRules?.length
|
||||
) {
|
||||
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
|
||||
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
|
||||
const ipRulesIncludeLegacyPortalBackend: boolean = legacyPortalBackendIPs.every((legacyPortalBackendIP: string) =>
|
||||
ipAddressesFromIPRules.includes(legacyPortalBackendIP),
|
||||
);
|
||||
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 = mongoProxyOutboundIPs.every((mongoProxyOutboundIP: string) =>
|
||||
ipAddressesFromIPRules.includes(mongoProxyOutboundIP),
|
||||
);
|
||||
|
||||
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 = cassandraProxyOutboundIPs.every(
|
||||
(cassandraProxyOutboundIP: string) => ipAddressesFromIPRules.includes(cassandraProxyOutboundIP),
|
||||
);
|
||||
|
||||
return !ipRulesIncludeCassandraProxy;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
import VcoreFirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -42,7 +43,11 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
return (
|
||||
<QuickstartFirewallNotification
|
||||
messageType={MessageTypes.OpenPostgresNetworkingBlade}
|
||||
screenshot={FirewallRuleScreenshot}
|
||||
screenshot={
|
||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
||||
? VcoreFirewallRuleScreenshot
|
||||
: FirewallRuleScreenshot
|
||||
}
|
||||
shellName={this.getShellNameForDisplay(this.kind)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React, { Component } from "react";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
@@ -192,29 +193,6 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
});
|
||||
}
|
||||
|
||||
private isValidId(id: string): boolean {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidStartCharacters = /^[/?#\\]/;
|
||||
if (invalidStartCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||
if (invalidMiddleCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||
if (invalidEndCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isNotEmpty(value: string): boolean {
|
||||
return !!value;
|
||||
}
|
||||
@@ -286,7 +264,13 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string,
|
||||
): void => {
|
||||
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
||||
let isValidId: boolean = true;
|
||||
if (inputElement) {
|
||||
isValidId = inputElement.reportValidity();
|
||||
}
|
||||
|
||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
||||
this.setState({ triggerId: newValue });
|
||||
};
|
||||
|
||||
@@ -313,7 +297,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
autoFocus
|
||||
required
|
||||
type="text"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new trigger id"
|
||||
size={40}
|
||||
value={triggerId}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { Label, TextField } from "@fluentui/react";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React, { Component } from "react";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
@@ -64,7 +65,13 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string,
|
||||
): void => {
|
||||
this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue);
|
||||
const inputElement = _event.currentTarget as HTMLInputElement;
|
||||
let isValidId: boolean = true;
|
||||
if (inputElement) {
|
||||
isValidId = inputElement.reportValidity();
|
||||
}
|
||||
|
||||
this.saveButton.enabled = this.isNotEmpty(newValue) && isValidId;
|
||||
this.setState({ udfId: newValue });
|
||||
};
|
||||
|
||||
@@ -238,29 +245,6 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
});
|
||||
}
|
||||
|
||||
private isValidId(id: string): boolean {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidStartCharacters = /^[/?#\\]/;
|
||||
if (invalidStartCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
||||
if (invalidMiddleCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
||||
if (invalidEndCharacters.test(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isNotEmpty(value: string): boolean {
|
||||
return !!value;
|
||||
}
|
||||
@@ -284,7 +268,8 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
required
|
||||
readOnly={!isUdfIdEditable}
|
||||
type="text"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new user defined function id"
|
||||
size={40}
|
||||
value={udfId}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -34,7 +35,6 @@ import QueryTablesTab from "../Tabs/QueryTablesTab";
|
||||
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||
import { useDatabases } from "../useDatabases";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
import { Platform, configContext } from "./../../ConfigContext";
|
||||
import ConflictId from "./ConflictId";
|
||||
import DocumentId from "./DocumentId";
|
||||
import StoredProcedure from "./StoredProcedure";
|
||||
@@ -210,7 +210,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
|
||||
const showScriptsMenus: boolean =
|
||||
configContext.platform != Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||
!isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||
this.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
|
||||
this.showTriggers = ko.observable<boolean>(showScriptsMenus);
|
||||
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
|
||||
import { Home16Regular } from "@fluentui/react-icons";
|
||||
import { AuthType } from "AuthType";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
|
||||
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import {
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
} from "Explorer/Tree/treeNodeUtil";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@@ -76,23 +76,22 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
||||
: [];
|
||||
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
|
||||
|
||||
const headerNodes: TreeNode[] =
|
||||
configContext.platform === Platform.Fabric
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "home",
|
||||
iconSrc: <Home16Regular />,
|
||||
label: "Home",
|
||||
isSelected: () =>
|
||||
useSelectedNode.getState().selectedNode === undefined &&
|
||||
useTabs.getState().activeReactTab === ReactTabKind.Home,
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
|
||||
},
|
||||
const headerNodes: TreeNode[] = isFabricMirrored()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "home",
|
||||
iconSrc: <Home16Regular />,
|
||||
label: "Home",
|
||||
isSelected: () =>
|
||||
useSelectedNode.getState().selectedNode === undefined &&
|
||||
useTabs.getState().activeReactTab === ReactTabKind.Home,
|
||||
onClick: () => {
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
|
||||
},
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
const rootNodes: TreeNode[] = useMemo(() => {
|
||||
if (sampleDataNodes.length > 0) {
|
||||
|
||||
@@ -740,7 +740,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = `
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric non read-only 1`] = `
|
||||
[
|
||||
{
|
||||
"children": [
|
||||
@@ -753,6 +753,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@@ -774,6 +780,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@@ -822,6 +834,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@@ -870,6 +888,12 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "Delete Container",
|
||||
"onClick": [Function],
|
||||
"styleClass": "deleteCollectionMenuItem",
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
@@ -915,6 +939,145 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric read-only 1`] = `
|
||||
[
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "standardCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "conflictsCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
],
|
||||
"className": "databaseNode",
|
||||
"contextMenu": undefined,
|
||||
"iconSrc": <DatabaseRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "standardDb",
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "sampleItemsCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
],
|
||||
"className": "databaseNode",
|
||||
"contextMenu": undefined,
|
||||
"iconSrc": <DatabaseRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "sharedDatabase",
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": undefined,
|
||||
"className": "collectionNode",
|
||||
"contextMenu": [
|
||||
{
|
||||
"iconSrc": {},
|
||||
"label": "New SQL Query",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"iconSrc": <DocumentMultipleRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "schemaCollection",
|
||||
"onClick": [Function],
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
{
|
||||
"className": "loadMoreNode",
|
||||
"label": "load more",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
"className": "databaseNode",
|
||||
"contextMenu": undefined,
|
||||
"iconSrc": <DatabaseRegular
|
||||
fontSize={16}
|
||||
/>,
|
||||
"isExpanded": true,
|
||||
"isSelected": [Function],
|
||||
"label": "giganticDatabase",
|
||||
"onCollapsed": [Function],
|
||||
"onContextMenuOpen": [Function],
|
||||
"onExpanded": [Function],
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Portal 1`] = `
|
||||
[
|
||||
{
|
||||
@@ -972,7 +1135,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
},
|
||||
],
|
||||
"isSelected": [Function],
|
||||
"label": "mockSproc3",
|
||||
"label": "mockSproc4",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
@@ -990,7 +1153,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
},
|
||||
],
|
||||
"isSelected": [Function],
|
||||
"label": "mockUdf3",
|
||||
"label": "mockUdf4",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
@@ -1008,7 +1171,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
||||
},
|
||||
],
|
||||
"isSelected": [Function],
|
||||
"label": "mockTrigger3",
|
||||
"label": "mockTrigger4",
|
||||
"onClick": [Function],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CapabilityNames } from "Common/Constants";
|
||||
import { Platform, updateConfigContext } from "ConfigContext";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
} from "Explorer/Tree/treeNodeUtil";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { updateUserContext } from "UserContext";
|
||||
import { FabricContext, updateUserContext, UserContext } from "UserContext";
|
||||
import PromiseSource from "Utils/PromiseSource";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -360,9 +361,30 @@ describe("createDatabaseTreeNodes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>]>([
|
||||
["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }],
|
||||
["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }],
|
||||
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
|
||||
[
|
||||
"the SQL API, on Fabric read-only",
|
||||
Platform.Fabric,
|
||||
false,
|
||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||
{ fabricContext: { isReadOnly: true } as FabricContext<CosmosDbArtifactType> },
|
||||
],
|
||||
[
|
||||
"the SQL API, on Fabric non read-only",
|
||||
Platform.Fabric,
|
||||
false,
|
||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||
{ fabricContext: { isReadOnly: false } as FabricContext<CosmosDbArtifactType> },
|
||||
],
|
||||
[
|
||||
"the SQL API, on Portal",
|
||||
Platform.Portal,
|
||||
false,
|
||||
{ capabilities: [], enableMultipleWriteLocations: true },
|
||||
{
|
||||
fabricContext: undefined,
|
||||
},
|
||||
],
|
||||
[
|
||||
"the Cassandra API, serverless, on Hosted",
|
||||
Platform.Hosted,
|
||||
@@ -373,6 +395,7 @@ describe("createDatabaseTreeNodes", () => {
|
||||
{ name: CapabilityNames.EnableServerless, description: "" },
|
||||
],
|
||||
},
|
||||
{ fabricContext: undefined },
|
||||
],
|
||||
[
|
||||
"the Mongo API, with Notebooks and Phoenix features, on Emulator",
|
||||
@@ -381,26 +404,31 @@ describe("createDatabaseTreeNodes", () => {
|
||||
{
|
||||
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
|
||||
},
|
||||
{ fabricContext: undefined },
|
||||
],
|
||||
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => {
|
||||
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
|
||||
updateConfigContext({ platform });
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
enableMultipleWriteLocations: true,
|
||||
...dbAccountProperties,
|
||||
},
|
||||
} as unknown as DataModels.DatabaseAccount,
|
||||
});
|
||||
const nodes = createDatabaseTreeNodes(
|
||||
explorer,
|
||||
isNotebookEnabled,
|
||||
useDatabases.getState().databases,
|
||||
refreshActiveTab,
|
||||
);
|
||||
expect(nodes).toMatchSnapshot();
|
||||
});
|
||||
])(
|
||||
"generates the correct tree structure for %s",
|
||||
(_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
|
||||
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
|
||||
updateConfigContext({ platform });
|
||||
updateUserContext({
|
||||
...userContext,
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
enableMultipleWriteLocations: true,
|
||||
...dbAccountProperties,
|
||||
},
|
||||
} as unknown as DataModels.DatabaseAccount,
|
||||
});
|
||||
const nodes = createDatabaseTreeNodes(
|
||||
explorer,
|
||||
isNotebookEnabled,
|
||||
useDatabases.getState().databases,
|
||||
refreshActiveTab,
|
||||
);
|
||||
expect(nodes).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
// The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes.
|
||||
// They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle.
|
||||
@@ -551,7 +579,18 @@ describe("createDatabaseTreeNodes", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["in Fabric", () => updateConfigContext({ platform: Platform.Fabric })],
|
||||
[
|
||||
"in Fabric",
|
||||
() => {
|
||||
updateConfigContext({ platform: Platform.Fabric });
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
||||
isReadOnly: true,
|
||||
} as FabricContext<CosmosDbArtifactType>,
|
||||
});
|
||||
},
|
||||
],
|
||||
[
|
||||
"for Cassandra API",
|
||||
() =>
|
||||
|
||||
@@ -6,6 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure";
|
||||
import Trigger from "Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
@@ -22,9 +23,7 @@ import { useNotebook } from "../Notebook/useNotebook";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
export const shouldShowScriptNodes = (): boolean => {
|
||||
return (
|
||||
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
|
||||
);
|
||||
return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||
};
|
||||
|
||||
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
|
||||
|
||||
@@ -1,48 +1,71 @@
|
||||
{
|
||||
"MaterializedViewsBuilderDescription": "Provision a Materializedviews builder cluster for your Azure Cosmos DB account. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"MaterializedViewsBuilder": "Materializedviews Builder",
|
||||
"Provisioned": "Provisioned",
|
||||
"Deprovisioned": "Deprovisioned",
|
||||
"LearnAboutMaterializedViews": "Learn more about materializedviews.",
|
||||
"DeprovisioningDetailsText": "Learn more about materializedviews.",
|
||||
"MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.",
|
||||
"SKUs": "SKUs",
|
||||
"SKUsPlaceHolder": "Select SKUs",
|
||||
"NumberOfInstances": "Number of instances",
|
||||
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
|
||||
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
|
||||
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
|
||||
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
|
||||
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
|
||||
"CreateMessage": "MaterializedViewsBuilder resource is being created.",
|
||||
"CreateInitializeTitle": "Provisioning resource",
|
||||
"CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.",
|
||||
"CreateSuccessTitle": "Resource provisioned",
|
||||
"CreateSuccesseMessage": "Materializedviews Builder resource provisioned.",
|
||||
"CreateFailureTitle": "Failed to provision resource",
|
||||
"CreateFailureMessage": "Materializedviews Builder resource provisioning failed.",
|
||||
"UpdateMessage": "MaterializedViewsBuilder resource is being updated.",
|
||||
"UpdateInitializeTitle": "Updating resource",
|
||||
"UpdateInitializeMessage": "Materializedviews Builder resource will be updated.",
|
||||
"UpdateSuccessTitle": "Resource updated",
|
||||
"UpdateSuccesseMessage": "Materializedviews Builder resource updated.",
|
||||
"UpdateFailureTitle": "Failed to update resource",
|
||||
"UpdateFailureMessage": "Materializedviews Builder resource updation failed.",
|
||||
"DeleteMessage": "MaterializedViewsBuilder resource is being deleted.",
|
||||
"DeleteInitializeTitle": "Deleting resource",
|
||||
"DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.",
|
||||
"DeleteSuccessTitle": "Resource deleted",
|
||||
"DeleteSuccesseMessage": "Materializedviews Builder resource deleted.",
|
||||
"DeleteFailureTitle": "Failed to delete resource",
|
||||
"DeleteFailureMessage": "Materializedviews Builder resource deletion failed.",
|
||||
"ApproximateCost": "Approximate Cost Per Hour",
|
||||
"CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.",
|
||||
"MetricsString": "Metrics",
|
||||
"MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ",
|
||||
"MetricsBlade": "the metrics blade.",
|
||||
"MonitorUsage": "Monitor Usage",
|
||||
"ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ",
|
||||
"ResizingDecisionLink": "learn more about Materializedviews Builder sizing.",
|
||||
"WarningBannerOnUpdate": "Adding or modifying Materializedviews Builder instances may affect your bill.",
|
||||
"WarningBannerOnDelete": "After deprovisioning the Materializedviews Builder, your materializedviews will not be updated with new source changes anymore. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition."
|
||||
"MaterializedViewsBuilderDescription": "Provision a materialized views builder cluster for your Azure Cosmos DB account. Materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"MaterializedViewsBuilder": "Materialized views Builder",
|
||||
"Provisioned": "Provisioned",
|
||||
"Deprovisioned": "Deprovisioned",
|
||||
"LearnAboutMaterializedViews": "Learn more about materialized views.",
|
||||
"DeprovisioningDetailsText": "Learn more about materialized views.",
|
||||
"MaterializedviewsBuilderPricing": "Learn more about materialized views pricing.",
|
||||
"SKUs": "SKUs",
|
||||
"SKUsPlaceHolder": "Select SKUs",
|
||||
"NumberOfInstances": "Number of instances",
|
||||
"CosmosD2s": "Cosmos.D2s (General Purpose Cosmos Compute with 2 vCPUs, 8 GB Memory)",
|
||||
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
|
||||
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
|
||||
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
|
||||
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
|
||||
"CreateMessage": "Materialized views builder resource is being created.",
|
||||
"CreateInitializeTitle": "Provisioning resource",
|
||||
"CreateInitializeMessage": "Materialized views Builder resource will be provisioned.",
|
||||
"CreateSuccessTitle": "Resource provisioned",
|
||||
"CreateSuccesseMessage": "Materialized views Builder resource provisioned.",
|
||||
"CreateFailureTitle": "Failed to provision resource",
|
||||
"CreateFailureMessage": "Materialized views Builder resource provisioning failed.",
|
||||
"UpdateMessage": "Materialized views builder resource is being updated.",
|
||||
"UpdateInitializeTitle": "Updating resource",
|
||||
"UpdateInitializeMessage": "Materialized views Builder resource will be updated.",
|
||||
"UpdateSuccessTitle": "Resource updated",
|
||||
"UpdateSuccesseMessage": "Materialized views Builder resource updated.",
|
||||
"UpdateFailureTitle": "Failed to update resource",
|
||||
"UpdateFailureMessage": "Materialized views Builder resource updation failed.",
|
||||
"DeleteMessage": "Materialized views builder resource is being deleted.",
|
||||
"DeleteInitializeTitle": "Deleting resource",
|
||||
"DeleteInitializeMessage": "Materialized views Builder resource will be deleted.",
|
||||
"DeleteSuccessTitle": "Resource deleted",
|
||||
"DeleteSuccesseMessage": "Materialized views Builder resource deleted.",
|
||||
"DeleteFailureTitle": "Failed to delete resource",
|
||||
"DeleteFailureMessage": "Materialized views Builder resource deletion failed.",
|
||||
"ApproximateCost": "Approximate Cost Per Hour",
|
||||
"CostText": "Hourly cost of the materialized views Builder resource depends on the SKU selection and number of instances per region.",
|
||||
"MetricsString": "Metrics",
|
||||
"MetricsText": "Monitor the CPU and memory usage for the materialized views Builder instances in ",
|
||||
"MetricsBlade": "the metrics blade.",
|
||||
"MonitorUsage": "Monitor Usage",
|
||||
"ResizingDecisionText": "To understand if the materialized views Builder is the right size, ",
|
||||
"ResizingDecisionLink": "learn more about materialized views Builder sizing.",
|
||||
"WarningBannerOnUpdate": "Adding or modifying materialized views Builder instances may affect your bill.",
|
||||
"WarningBannerOnDelete": "After deprovisioning the materialized views Builder, your materialized views will not be updated with new source changes anymore. materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.",
|
||||
"GlobalsecondaryindexesBuilderDescription": "Provision a global secondary indexes builder for your Azure Cosmos DB account. The global secondary indexes builder is compute in your account that performs read operations on source collections for any updates and populates the global secondary indexes as per their definition.",
|
||||
"GlobalsecondaryindexesBuilder": "Global secondary indexes builder",
|
||||
"LearnAboutGlobalSecondaryIndexes": "Learn more about global secondary indexes.",
|
||||
"GlobalsecondaryindexesDeprovisioningDetailsText": "Learn more about global secondary indexes.",
|
||||
"GlobalsecondaryindexesBuilderPricing": "Learn more about global secondary indexes pricing.",
|
||||
"GlobalsecondaryindexesCreateMessage": "Global secondary indexes builder resource is being created.",
|
||||
"GlobalsecondaryindexesCreateInitializeMessage": "Global secondary indexes builder resource will be provisioned.",
|
||||
"GlobalsecondaryindexesCreateSuccesseMessage": "Global secondary indexes builder resource provisioned.",
|
||||
"GlobalsecondaryindexesCreateFailureMessage": "Global secondary indexes builder resource provisioning failed.",
|
||||
"GlobalsecondaryindexesUpdateMessage": "Global secondary indexes builder resource is being updated.",
|
||||
"GlobalsecondaryindexesUpdateInitializeMessage": "Global secondary indexes builder resource will be updated.",
|
||||
"GlobalsecondaryindexesUpdateSuccesseMessage": "Global secondary indexes builder resource updated.",
|
||||
"GlobalsecondaryindexesUpdateFailureMessage": "Global secondary indexes builder resource update failed.",
|
||||
"GlobalsecondaryindexesDeleteMessage": "Global secondary indexes builder resource is being deleted.",
|
||||
"GlobalsecondaryindexesDeleteInitializeMessage": "Global secondary indexes builder resource will be deleted.",
|
||||
"GlobalsecondaryindexesDeleteSuccesseMessage": "Global secondary indexes builder resource deleted.",
|
||||
"GlobalsecondaryindexesDeleteFailureMessage": "Global secondary indexes builder resource deletion failed.",
|
||||
"GlobalsecondaryindexesCostText": "Hourly cost of the global secondary indexes builder resource depends on the SKU selection and number of instances per region.",
|
||||
"GlobalsecondaryindexesMetricsText": "Monitor the CPU and memory usage for the global secondary indexes builder instances in ",
|
||||
"GlobalsecondaryindexesResizingDecisionText": "To understand if the global secondary indexes builder is the right size, ",
|
||||
"GlobalsecondaryindexesesizingDecisionLink": "learn more about global secondary indexes builder sizing.",
|
||||
"GlobalsecondaryindexesWarningBannerOnUpdate": "Adding or modifying global secondary indexes builder instances may affect your bill.",
|
||||
"GlobalsecondaryindexesWarningBannerOnDelete": "After deprovisioning the global secondary indexes builder, your global secondary indexes will no longer be updated with new source changes. Global secondary indexes builder is compute in your account that performs read operations on source collection for any updates and applies them on global secondary indexes as per their definition."
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { userContext } from "UserContext";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
@@ -35,21 +35,26 @@ import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
export class PhoenixClient {
|
||||
private armResourceId: string;
|
||||
private containerHealthHandler: NodeJS.Timeout;
|
||||
private retryOptions: promiseRetry.Options = {
|
||||
private retryOptions: Options = {
|
||||
retries: Notebook.retryAttempts,
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
private abortController: AbortController;
|
||||
private abortSignal: AbortSignal;
|
||||
|
||||
constructor(armResourceId: string) {
|
||||
this.armResourceId = armResourceId;
|
||||
}
|
||||
|
||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||
this.initializeCancelEventListener();
|
||||
|
||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
||||
retries: 4,
|
||||
maxTimeout: 20000,
|
||||
minTimeout: 20000,
|
||||
signal: this.abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,6 +275,17 @@ export class PhoenixClient {
|
||||
};
|
||||
}
|
||||
|
||||
private initializeCancelEventListener(): void {
|
||||
this.abortController = new AbortController();
|
||||
this.abortSignal = this.abortController.signal;
|
||||
|
||||
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey && (event.key === "c" || event.key === "z")) {
|
||||
this.abortController.abort(new AbortError("Request canceled"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
|
||||
const errInfo = jsonData;
|
||||
switch (errInfo?.type) {
|
||||
|
||||
@@ -1,56 +1,112 @@
|
||||
import { sendCachedDataMessage } from "Common/MessageHandler";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
|
||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||
import { updateUserContext, userContext } from "UserContext";
|
||||
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
||||
import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
|
||||
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 | undefined;
|
||||
|
||||
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
|
||||
let lastRequestTimestamp: number = undefined;
|
||||
let lastRequestTimestamp: number | undefined = undefined;
|
||||
|
||||
const requestDatabaseResourceTokens = async (): Promise<void> => {
|
||||
/**
|
||||
* Request fabric token:
|
||||
* - Mirrored key and AAD: Database Resource Tokens
|
||||
* - Native: AAD token
|
||||
* @returns
|
||||
*/
|
||||
const requestFabricToken = async (): Promise<void> => {
|
||||
if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastRequestTimestamp = Date.now();
|
||||
try {
|
||||
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
|
||||
FabricMessageTypes.GetAllResourceTokens,
|
||||
[],
|
||||
userContext.fabricContext.connectionId,
|
||||
);
|
||||
|
||||
if (!userContext.databaseAccount.properties.documentEndpoint) {
|
||||
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
|
||||
if (isFabricMirrored()) {
|
||||
await requestAndStoreDatabaseResourceTokens();
|
||||
} else if (isFabricNative()) {
|
||||
await requestAndStoreAccessToken();
|
||||
}
|
||||
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
databaseConnectionInfo: fabricDatabaseConnectionInfo,
|
||||
isReadOnly: true,
|
||||
},
|
||||
databaseAccount: { ...userContext.databaseAccount },
|
||||
});
|
||||
scheduleRefreshDatabaseResourceToken();
|
||||
scheduleRefreshFabricToken();
|
||||
} catch (error) {
|
||||
logConsoleError(error);
|
||||
logConsoleError(error as string);
|
||||
throw error;
|
||||
} finally {
|
||||
lastRequestTimestamp = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const requestAndStoreDatabaseResourceTokens = async (): Promise<void> => {
|
||||
if (!userContext.fabricContext || !userContext.databaseAccount) {
|
||||
// This should not happen
|
||||
logConsoleError("Fabric context or database account is missing: cannot request tokens");
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceTokenInfo = await sendCachedDataMessage<ResourceTokenInfo>(
|
||||
FabricMessageTypes.GetAllResourceTokens,
|
||||
[],
|
||||
userContext.fabricContext.artifactInfo?.connectionId,
|
||||
);
|
||||
|
||||
if (!userContext.databaseAccount.properties.documentEndpoint) {
|
||||
userContext.databaseAccount.properties.documentEndpoint = resourceTokenInfo.endpoint;
|
||||
}
|
||||
|
||||
if (resourceTokenInfo.credentialType === "OAuth2") {
|
||||
// Mirrored AAD
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
databaseName: resourceTokenInfo.databaseId,
|
||||
artifactInfo: undefined,
|
||||
isReadOnly: resourceTokenInfo.isReadOnly ?? userContext.fabricContext.isReadOnly,
|
||||
},
|
||||
databaseAccount: { ...userContext.databaseAccount },
|
||||
aadToken: resourceTokenInfo.accessToken,
|
||||
});
|
||||
} else {
|
||||
// TODO: In Fabric contract V2, credentialType is undefined. For V3, it is "Key". Check for "Key" when V3 is supported for Fabric Mirroring Key
|
||||
// Mirrored key
|
||||
updateUserContext({
|
||||
fabricContext: {
|
||||
...userContext.fabricContext,
|
||||
databaseName: resourceTokenInfo.databaseId,
|
||||
artifactInfo: {
|
||||
...(userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]),
|
||||
resourceTokenInfo,
|
||||
},
|
||||
isReadOnly: resourceTokenInfo.isReadOnly ?? userContext.fabricContext.isReadOnly,
|
||||
},
|
||||
databaseAccount: { ...userContext.databaseAccount },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const requestAndStoreAccessToken = async (): Promise<void> => {
|
||||
if (!userContext.fabricContext || !userContext.databaseAccount) {
|
||||
// This should not happen
|
||||
logConsoleError("Fabric context or database account is missing: cannot request tokens");
|
||||
return;
|
||||
}
|
||||
|
||||
const accessTokenInfo = await sendCachedDataMessage<{ accessToken: string }>(FabricMessageTypes.GetAccessToken, []);
|
||||
|
||||
updateUserContext({
|
||||
aadToken: accessTokenInfo.accessToken,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check token validity and schedule a refresh if necessary
|
||||
* @param tokenTimestamp
|
||||
* @returns
|
||||
*/
|
||||
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
|
||||
export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -59,7 +115,7 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
|
||||
|
||||
timeoutId = setTimeout(
|
||||
() => {
|
||||
requestDatabaseResourceTokens().then(resolve);
|
||||
requestFabricToken().then(resolve);
|
||||
},
|
||||
refreshNow ? 0 : TOKEN_VALIDITY_MS,
|
||||
);
|
||||
@@ -68,6 +124,15 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
|
||||
|
||||
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
|
||||
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
|
||||
scheduleRefreshDatabaseResourceToken(true);
|
||||
scheduleRefreshFabricToken(true);
|
||||
}
|
||||
};
|
||||
|
||||
export const isFabric = (): boolean => configContext.platform === Platform.Fabric;
|
||||
export const isFabricMirroredKey = (): boolean =>
|
||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_KEY;
|
||||
export const isFabricMirroredAAD = (): boolean =>
|
||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_AAD;
|
||||
export const isFabricMirrored = (): boolean => isFabricMirroredKey() || isFabricMirroredAAD();
|
||||
export const isFabricNative = (): boolean =>
|
||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.NATIVE;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
|
||||
import * as React from "react";
|
||||
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
|
||||
import ErrorImage from "../../../../images/error.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import { BackendApi, HttpHeaders } from "../../../Common/Constants";
|
||||
import { HttpHeaders } from "../../../Common/Constants";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
|
||||
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
|
||||
|
||||
interface Props {
|
||||
@@ -19,10 +17,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
|
||||
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
|
||||
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken";
|
||||
@@ -35,28 +29,10 @@ export const fetchEncryptedToken = async (connectionString: string): Promise<str
|
||||
return decodeURIComponent(encryptedTokenResponse);
|
||||
};
|
||||
|
||||
export const fetchEncryptedToken_ToBeDeprecated = async (connectionString: string): Promise<string> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
}
|
||||
// This API has a quirk where it must be parsed twice
|
||||
const result: GenerateTokenResponse = JSON.parse(await response.json());
|
||||
return decodeURIComponent(result.readWrite || result.read);
|
||||
};
|
||||
|
||||
export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
|
||||
const headers = new Headers();
|
||||
headers.append(HttpHeaders.connectionString, connectionString);
|
||||
|
||||
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.AccountRestrictions)
|
||||
? configContext.PORTAL_BACKEND_ENDPOINT
|
||||
: configContext.BACKEND_ENDPOINT;
|
||||
|
||||
const url = backendEndpoint + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
||||
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
|
||||
const response = await fetch(url, { headers, method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw response;
|
||||
|
||||
@@ -16,6 +16,7 @@ export type Features = {
|
||||
readonly enableAadDataPlane: boolean;
|
||||
readonly enableResourceGraph: boolean;
|
||||
readonly enableKoResourceTree: boolean;
|
||||
readonly enableThroughputBuckets: boolean;
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
readonly phoenixEndpoint?: string;
|
||||
@@ -81,6 +82,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
enableSpark: "true" === get("enablespark"),
|
||||
enableTtl: "true" === get("enablettl"),
|
||||
enableKoResourceTree: "true" === get("enablekoresourcetree"),
|
||||
enableThroughputBuckets: "true" === get("enablethroughputbuckets"),
|
||||
executeSproc: "true" === get("dataexplorerexecutesproc"),
|
||||
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
|
||||
mongoProxyEndpoint: get("mongoproxyendpoint"),
|
||||
|
||||
@@ -6,9 +6,9 @@ import { RefreshResult } from "../SelfServeTypes";
|
||||
import MaterializedViewsBuilder from "./MaterializedViewsBuilder";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
MaterializedViewsBuilderServiceResource,
|
||||
PriceMapAndCurrencyCode,
|
||||
RegionsResponse,
|
||||
MaterializedViewsBuilderServiceResource,
|
||||
UpdateMaterializedViewsBuilderRequestParameters,
|
||||
} from "./MaterializedViewsBuilderTypes";
|
||||
|
||||
@@ -123,11 +123,23 @@ export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<Ref
|
||||
if (response.properties.status === ResourceStatus.Running.toString()) {
|
||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
|
||||
return {
|
||||
isUpdateInProgress: true,
|
||||
updateInProgressMessageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateMessage" : "CreateMessage",
|
||||
};
|
||||
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
|
||||
return {
|
||||
isUpdateInProgress: true,
|
||||
updateInProgressMessageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteMessage" : "DeleteMessage",
|
||||
};
|
||||
} else {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
|
||||
return {
|
||||
isUpdateInProgress: true,
|
||||
updateInProgressMessageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateMessage" : "UpdateMessage",
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
//TODO differentiate between different failures
|
||||
|
||||
@@ -29,17 +29,20 @@ import {
|
||||
updateMaterializedViewsBuilderResource,
|
||||
} from "./MaterializedViewsBuilder.rp";
|
||||
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
const costPerHourDefaultValue: Description = {
|
||||
textTKey: "CostText",
|
||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
const metricsStringValue: Description = {
|
||||
textTKey: "MetricsText",
|
||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesMetricsText" : "MetricsText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: generateBladeLink(BladeType.Metrics),
|
||||
@@ -76,7 +79,8 @@ const onNumberOfInstancesChange = (
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
@@ -116,7 +120,8 @@ const onEnableMaterializedViewsBuilderChange = (
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
@@ -129,10 +134,17 @@ const onEnableMaterializedViewsBuilderChange = (
|
||||
} else {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnDelete",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesWarningBannerOnDelete" : "WarningBannerOnDelete",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||
textTKey: "DeprovisioningDetailsText",
|
||||
href:
|
||||
userContext.apiType === "SQL"
|
||||
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
|
||||
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesDeprovisioningDetailsText"
|
||||
: "DeprovisioningDetailsText",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
@@ -182,18 +194,19 @@ const getInstancesMax = async (): Promise<number> => {
|
||||
};
|
||||
|
||||
const NumberOfInstancesDropdownInfo: Info = {
|
||||
messageTKey: "ResizingDecisionText",
|
||||
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesResizingDecisionText" : "ResizingDecisionText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
|
||||
textTKey: "ResizingDecisionLink",
|
||||
textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesesizingDecisionLink" : "ResizingDecisionLink",
|
||||
},
|
||||
};
|
||||
|
||||
const ApproximateCostDropDownInfo: Info = {
|
||||
messageTKey: "CostText",
|
||||
messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
|
||||
textTKey: "MaterializedviewsBuilderPricing",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -268,15 +281,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "DeleteInitializeTitle",
|
||||
messageTKey: "DeleteInitializeMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesDeleteInitializeMessage"
|
||||
: "DeleteInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "DeleteSuccessTitle",
|
||||
messageTKey: "DeleteSuccesseMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteSuccesseMessage" : "DeleteSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "DeleteFailureTitle",
|
||||
messageTKey: "DeleteFailureMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteFailureMessage" : "DeleteFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -289,15 +307,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "UpdateInitializeTitle",
|
||||
messageTKey: "UpdateInitializeMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesUpdateInitializeMessage"
|
||||
: "UpdateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "UpdateSuccessTitle",
|
||||
messageTKey: "UpdateSuccesseMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateSuccesseMessage" : "UpdateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "UpdateFailureTitle",
|
||||
messageTKey: "UpdateFailureMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateFailureMessage" : "UpdateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -311,15 +334,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "CreateInitializeTitle",
|
||||
messageTKey: "CreateInitializeMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesCreateInitializeMessage"
|
||||
: "CreateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "CreateSuccessTitle",
|
||||
messageTKey: "CreateSuccesseMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateSuccesseMessage" : "CreateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "CreateFailureTitle",
|
||||
messageTKey: "CreateFailureMessage",
|
||||
messageTKey:
|
||||
userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateFailureMessage" : "CreateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -366,11 +394,17 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
|
||||
@Values({
|
||||
description: {
|
||||
textTKey: "MaterializedViewsBuilderDescription",
|
||||
textTKey:
|
||||
userContext.apiType === "SQL"
|
||||
? "GlobalsecondaryindexesBuilderDescription"
|
||||
: "MaterializedViewsBuilderDescription",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-materializedviews",
|
||||
textTKey: "LearnAboutMaterializedViews",
|
||||
href:
|
||||
userContext.apiType === "SQL"
|
||||
? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views"
|
||||
: "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views",
|
||||
textTKey: userContext.apiType === "SQL" ? "LearnAboutGlobalSecondaryIndexes" : "LearnAboutMaterializedViews",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -378,7 +412,7 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
|
||||
|
||||
@OnChange(onEnableMaterializedViewsBuilderChange)
|
||||
@Values({
|
||||
labelTKey: "MaterializedViewsBuilder",
|
||||
labelTKey: userContext.apiType === "SQL" ? "GlobalSecondaryIndexesBuilder" : "MaterializedViewsBuilder",
|
||||
trueLabelTKey: "Provisioned",
|
||||
falseLabelTKey: "Deprovisioned",
|
||||
})
|
||||
|
||||
@@ -11,13 +11,24 @@ import { updateUserContext } from "../UserContext";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import "./SelfServe.less";
|
||||
import { SelfServeComponent } from "./SelfServeComponent";
|
||||
import { SelfServeDescriptor } from "./SelfServeTypes";
|
||||
import { SelfServeBaseClass, SelfServeDescriptor } from "./SelfServeTypes";
|
||||
import { SelfServeType } from "./SelfServeUtils";
|
||||
initializeIcons();
|
||||
|
||||
const loadTranslationFile = async (className: string): Promise<void> => {
|
||||
const loadTranslationFile = async (
|
||||
className: string | SelfServeBaseClass,
|
||||
selfServeType?: SelfServeType,
|
||||
): Promise<void> => {
|
||||
const language = i18n.languages[0];
|
||||
const fileName = `${className}.json`;
|
||||
let namespace: string; // className is used as a key to retrieve the localized strings
|
||||
let fileName: string;
|
||||
if (className instanceof SelfServeBaseClass) {
|
||||
fileName = `${selfServeType}.json`;
|
||||
namespace = className.constructor.name;
|
||||
} else {
|
||||
fileName = `${className}.json`;
|
||||
namespace = className;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let translations: any;
|
||||
@@ -28,12 +39,16 @@ const loadTranslationFile = async (className: string): Promise<void> => {
|
||||
} catch (e) {
|
||||
translations = await import(/* webpackChunkName: "Localization-en-[request]" */ `../Localization/en/${fileName}`);
|
||||
}
|
||||
i18n.addResourceBundle(language, className, translations.default, true);
|
||||
|
||||
i18n.addResourceBundle(language, namespace, translations.default, true);
|
||||
};
|
||||
|
||||
const loadTranslations = async (className: string): Promise<void> => {
|
||||
const loadTranslations = async (
|
||||
className: string | SelfServeBaseClass,
|
||||
selfServeType: SelfServeType,
|
||||
): Promise<void> => {
|
||||
await loadTranslationFile("Common");
|
||||
await loadTranslationFile(className);
|
||||
await loadTranslationFile(className, selfServeType);
|
||||
};
|
||||
|
||||
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
||||
@@ -41,13 +56,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
case SelfServeType.example: {
|
||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||
const selfServeExample = new SelfServeExample.default();
|
||||
await loadTranslations(selfServeExample.constructor.name);
|
||||
await loadTranslations(selfServeExample, selfServeType);
|
||||
return selfServeExample.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.sqlx: {
|
||||
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||
const sqlX = new SqlX.default();
|
||||
await loadTranslations(sqlX.constructor.name);
|
||||
await loadTranslations(sqlX, selfServeType);
|
||||
return sqlX.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.graphapicompute: {
|
||||
@@ -55,7 +70,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||
);
|
||||
const graphAPICompute = new GraphAPICompute.default();
|
||||
await loadTranslations(graphAPICompute.constructor.name);
|
||||
await loadTranslations(graphAPICompute, selfServeType);
|
||||
return graphAPICompute.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.materializedviewsbuilder: {
|
||||
@@ -63,7 +78,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
|
||||
);
|
||||
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
|
||||
await loadTranslations(materializedViewsBuilder.constructor.name);
|
||||
await loadTranslations(materializedViewsBuilder, selfServeType);
|
||||
return materializedViewsBuilder.toSelfServeDescriptor();
|
||||
}
|
||||
default:
|
||||
@@ -103,7 +118,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType;
|
||||
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType];
|
||||
const selfServeType = SelfServeType[selfServeTypeText.toLocaleLowerCase() as keyof typeof SelfServeType];
|
||||
if (
|
||||
!inputs.subscriptionId ||
|
||||
!inputs.resourceGroup ||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { TFunction } from "i18next";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import React from "react";
|
||||
import { WithTranslation } from "react-i18next";
|
||||
import * as _ from "underscore";
|
||||
@@ -80,7 +80,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
private static readonly defaultRetryIntervalInMs = 30000;
|
||||
private smartUiGeneratorClassName: string;
|
||||
private retryIntervalInMs: number;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private retryOptions: Options;
|
||||
private translationFunction: TFunction;
|
||||
|
||||
componentDidMount(): void {
|
||||
|
||||
@@ -29,10 +29,11 @@ export enum SelfServeType {
|
||||
// Unsupported self serve type passed as feature flag
|
||||
invalid = "invalid",
|
||||
// Add your self serve types here
|
||||
example = "example",
|
||||
sqlx = "sqlx",
|
||||
graphapicompute = "graphapicompute",
|
||||
materializedviewsbuilder = "materializedviewsbuilder",
|
||||
// NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\
|
||||
example = "SelfServeExample",
|
||||
sqlx = "SqlX",
|
||||
graphapicompute = "GraphAPICompute",
|
||||
materializedviewsbuilder = "MaterializedViewsBuilder",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -197,6 +197,11 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
let billingCurrency;
|
||||
for (const region of map.keys()) {
|
||||
// if no offering id is found for that region, skipping calling price API
|
||||
const subMap = map.get(region);
|
||||
if (!subMap || subMap.size === 0) {
|
||||
continue;
|
||||
}
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
const regionShortName = await getRegionShortName(region);
|
||||
const requestBody: OfferingIdRequest = {
|
||||
@@ -237,7 +242,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||
return { priceMap: undefined, billingCurrency: undefined };
|
||||
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,6 +291,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||
return undefined;
|
||||
return new Map<string, Map<string, string>>();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -227,11 +227,13 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
let costPerHour = 0;
|
||||
let costBreakdown = "";
|
||||
for (const regionItem of regions) {
|
||||
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
|
||||
const incrementalCost = priceMap?.get(regionItem.locationName)?.get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error(`${regionItem.locationName} not found in price map.`);
|
||||
} else if (incrementalCost === 0) {
|
||||
throw new Error(`${regionItem.locationName} cost per hour = 0`);
|
||||
} else if (currencyCode === undefined) {
|
||||
throw new Error(`Currency code not found in price map.`);
|
||||
}
|
||||
|
||||
let regionalInstanceCount = instanceCount;
|
||||
|
||||
@@ -17,7 +17,7 @@ export class JupyterLabAppFactory {
|
||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
||||
this.restartShell = true;
|
||||
}
|
||||
return content?.includes("cosmosuser@");
|
||||
return content?.includes("cosmosshelluser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
@@ -68,7 +68,6 @@ export class JupyterLabAppFactory {
|
||||
const session = await manager.startNew();
|
||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
||||
const content = message.content && message.content[0]?.toString();
|
||||
|
||||
if (this.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
@@ -114,6 +113,13 @@ export class JupyterLabAppFactory {
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
// Close terminal when Ctrl key is pressed
|
||||
term.node.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey) {
|
||||
this.onShellExited(false);
|
||||
}
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -47,11 +47,21 @@ export interface VCoreMongoConnectionParams {
|
||||
connectionString: string;
|
||||
}
|
||||
|
||||
interface FabricContext {
|
||||
connectionId: string;
|
||||
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||
export interface FabricArtifactInfo {
|
||||
[CosmosDbArtifactType.MIRRORED_KEY]: {
|
||||
connectionId: string;
|
||||
resourceTokenInfo: ResourceTokenInfo | undefined;
|
||||
};
|
||||
[CosmosDbArtifactType.MIRRORED_AAD]: undefined;
|
||||
[CosmosDbArtifactType.NATIVE]: undefined;
|
||||
}
|
||||
export interface FabricContext<T extends CosmosDbArtifactType> {
|
||||
fabricClientRpcVersion: string;
|
||||
isReadOnly: boolean;
|
||||
isVisible: boolean;
|
||||
databaseName: string;
|
||||
artifactType: CosmosDbArtifactType;
|
||||
artifactInfo: FabricArtifactInfo[T];
|
||||
}
|
||||
|
||||
export type AdminFeedbackControlPolicy =
|
||||
@@ -70,7 +80,7 @@ export type AdminFeedbackPolicySettings = {
|
||||
};
|
||||
|
||||
export interface UserContext {
|
||||
readonly fabricContext?: FabricContext;
|
||||
readonly fabricContext?: FabricContext<CosmosDbArtifactType>;
|
||||
readonly authType?: AuthType;
|
||||
readonly masterKey?: string;
|
||||
readonly subscriptionId?: string;
|
||||
|
||||
@@ -89,3 +89,7 @@ export const getItemName = (): string => {
|
||||
return "Items";
|
||||
}
|
||||
};
|
||||
|
||||
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
||||
return apiType === "SQL" || apiType === "Tables";
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ describe("AuthorizationUtils", () => {
|
||||
it("should throw an error if token is malformed", () => {
|
||||
expect(() =>
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
// This is an invalid JWT token used for testing
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.",
|
||||
),
|
||||
).toThrow();
|
||||
@@ -47,6 +48,7 @@ describe("AuthorizationUtils", () => {
|
||||
it("should return decrypted token payload", () => {
|
||||
expect(
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
// This is an expired JWT token used for testing
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ",
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const autoPilotThroughput1K = 1000;
|
||||
export const autoPilotIncrementStep = 1000;
|
||||
export const autoPilotThroughput4K = 4000;
|
||||
export const autoPilotThroughput10K = 10000;
|
||||
|
||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||
if (!maxThroughput) {
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
@@ -73,9 +66,6 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
|
||||
//"https://main2.documentdb.ext.azure.com": ["104.42.196.69"],
|
||||
"https://main.documentdb.ext.azure.cn": ["139.217.8.252"],
|
||||
"https://main.documentdb.ext.azure.us": ["52.244.48.71"],
|
||||
// Add ussec and usnat when endpoint address is known:
|
||||
//ussec: ["29.26.26.67", "29.26.26.66"],
|
||||
//usnat: ["7.28.202.68"],
|
||||
};
|
||||
|
||||
export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
|
||||
@@ -100,14 +90,6 @@ export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
MongoProxyEndpoints.Mooncake,
|
||||
];
|
||||
|
||||
export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://main.cosmos.ext.azure",
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
|
||||
CassandraProxyEndpoints.Development,
|
||||
CassandraProxyEndpoints.Mpac,
|
||||
@@ -153,53 +135,3 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
|
||||
//
|
||||
// Temporary function to determine if a portal backend API is supported by the
|
||||
// new backend in this environment.
|
||||
//
|
||||
// TODO: Remove this function once new backend migration is completed for all environments.
|
||||
//
|
||||
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
|
||||
// This maps backend APIs to the environments supported by the new backend.
|
||||
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
|
||||
[BackendApi.GenerateToken]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.PortalSettings]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.AccountRestrictions]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.RuntimeProxy]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
[BackendApi.DisallowedLocations]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
PortalBackendEndpoints.Fairfax,
|
||||
PortalBackendEndpoints.Mooncake,
|
||||
],
|
||||
[BackendApi.SampleData]: [
|
||||
PortalBackendEndpoints.Development,
|
||||
PortalBackendEndpoints.Mpac,
|
||||
PortalBackendEndpoints.Prod,
|
||||
],
|
||||
};
|
||||
|
||||
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newBackendApiEnvironmentMap[backendApi].includes(configContext.PORTAL_BACKEND_ENDPOINT);
|
||||
}
|
||||
|
||||
18
src/Utils/ValidationUtils.test.ts
Normal file
18
src/Utils/ValidationUtils.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsValidCosmosDbResourceId } from "Utils/ValidationUtils";
|
||||
|
||||
const testCases = [
|
||||
["validId", true],
|
||||
["forward/slash", false],
|
||||
["back\\slash", false],
|
||||
["question?mark", false],
|
||||
["hash#mark", false],
|
||||
["?invalidstart", false],
|
||||
["invalidEnd/", false],
|
||||
["space-at-end ", false],
|
||||
];
|
||||
|
||||
describe("IsValidCosmosDbResourceId", () => {
|
||||
test.each(testCases)("IsValidCosmosDbResourceId(%p). Expected: %p", (id: string, expected: boolean) => {
|
||||
expect(IsValidCosmosDbResourceId(id)).toBe(expected);
|
||||
});
|
||||
});
|
||||
24
src/Utils/ValidationUtils.ts
Normal file
24
src/Utils/ValidationUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Common methods and constants for validation
|
||||
//
|
||||
|
||||
//
|
||||
// Validation of id for Cosmos DB resources:
|
||||
// - Database
|
||||
// - Container
|
||||
// - Stored Procedure
|
||||
// - User Defined Function (UDF)
|
||||
// - Trigger
|
||||
//
|
||||
// Use these with <input> elements
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
export const ValidCosmosDbIdInputPattern: RegExp = /[^\/?#\\]*[^\/?# \\]/;
|
||||
export const ValidCosmosDbIdDescription: string = "May not end with space nor contain characters '\\' '/' '#' '?'";
|
||||
|
||||
// For a standalone function regex, we need to wrap the previous reg expression,
|
||||
// to test against the entire value. This is done implicitly by input elements.
|
||||
const ValidCosmosDbIdRegex: RegExp = new RegExp(`^(?:${ValidCosmosDbIdInputPattern.source})$`);
|
||||
|
||||
export function IsValidCosmosDbResourceId(id: string): boolean {
|
||||
return id && ValidCosmosDbIdRegex.test(id);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { Platform, configContext } from "./../ConfigContext";
|
||||
|
||||
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
|
||||
@@ -7,7 +8,7 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
|
||||
if (currentWindow.parent === currentWindow) {
|
||||
return undefined;
|
||||
}
|
||||
if (configContext.platform === Platform.Fabric && currentWindow.parent.parent === currentWindow.top) {
|
||||
if (isFabric() && currentWindow.parent.parent === currentWindow.top) {
|
||||
// in Fabric data explorer is inside an extension iframe, so we have two parent iframes
|
||||
return currentWindow;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
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/2024-02-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
|
||||
/* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
|
||||
export async function listCassandraKeyspaces(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
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/2024-02-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given database account and collection. */
|
||||
export async function listMetrics(
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
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/2024-02-15-preview/cosmos-db.json
|
||||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-12-01-preview/cosmos-db.json
|
||||
*/
|
||||
|
||||
import { configContext } from "../../../../ConfigContext";
|
||||
import { armRequest } from "../../request";
|
||||
import * as Types from "./types";
|
||||
const apiVersion = "2024-02-15-preview";
|
||||
const apiVersion = "2024-12-01-preview";
|
||||
|
||||
/* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
|
||||
export async function listMetrics(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user