Compare commits

...

43 Commits

Author SHA1 Message Date
Asier Isayas
7224dd26c1 update settings component test snap 2025-03-17 13:51:46 -04:00
Asier Isayas
4c73a1cc47 update treeNodeUtil test snap 2025-03-17 13:45:46 -04:00
Asier Isayas
610da6a9a5 fix tests 2025-03-17 13:37:37 -04:00
Nishtha Ahuja
7812ca4914 updated tests (#2077)
Co-authored-by: nishthaAhujaa
2025-03-17 22:45:51 +05:30
Nishtha Ahuja
bc4f18ba79 Panel Integration (#2075)
* integrated panel

* edited header text

---------

Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-03-14 15:11:36 -04:00
Asier Isayas
fd2551423d merge conflict 2025-03-14 14:29:21 -04:00
Asier Isayas
508abcd21c Merge branch 'feature/materialized-views' of https://github.com/Azure/cosmos-explorer into feature/materialized-views 2025-03-14 14:28:36 -04:00
Asier Isayas
7774589d60 fix tests 2025-03-14 14:28:28 -04:00
Asier Isayas
e23ba5ec8c fix type error 2025-03-14 13:55:00 -04:00
Nishtha Ahuja
75719b3cf0 test files (#2074)
Co-authored-by: nishthaAhujaa
2025-03-13 11:01:34 +05:30
Asier Isayas
f3f8fd241a Merge branch 'master' of https://github.com/Azure/cosmos-explorer into feature/materialized-views 2025-03-12 16:52:21 -04:00
Asier Isayas
2dc2e59162 tests 2025-03-12 16:37:24 -04:00
SATYA SB
4b65760a1d [accessibility-3554312-3560235]:[Screen reader - Cosmos DB Query Copilot - Query Faster with Copilot>Enable Query Advisor]: Screen reader does not announce the associated text information when focus lands on the 'Like/Dislike' button. (#2067)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-03-11 12:31:51 +05:30
SATYA SB
ced2725476 Enhance accessibility and focus styles for Notification Console component (#2066)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-03-11 12:28:44 +05:30
Asier Isayas
6b811b5e76 add tests 2025-03-10 14:56:55 -04:00
Asier Isayas
69cf523274 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into feature/materialized-views 2025-03-10 14:09:47 -04:00
Asier Isayas
2e45d8a2a4 styling 2025-03-10 13:49:42 -04:00
asier-isayas
b5d7423849 Set default RU throughput for Production workload accounts to be 10k (#2070)
* assign default throughput based on workload type

* combined common logic

* fix unit tests

* add tests

* update tests

* npm run format

* Set default RU throughput for Production workload accounts to be 10k

* remove unused method

* refactor

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-03-10 11:35:17 -04:00
Laurent Nguyen
1529303107 Fabric native: use SDK not ARM for update offers/collections. Enable Delete Container context menu item in resource tree (#2069)
* For all control plane operations, do not use ARM for Fabric. Enable "delete container" for fabric native.

* Fix unit test

* Fix tre note tests with proper fabric config. Add new fabric non-readonly test.
2025-03-07 07:10:45 +01:00
Asier Isayas
8624bf0423 format 2025-03-06 15:21:58 -05:00
Asier Isayas
db1600d81b format 2025-03-06 15:12:53 -05:00
Asier Isayas
176bb47cb5 Merge branch 'feature/materialized-views' of https://github.com/Azure/cosmos-explorer into feature/materialized-views 2025-03-06 15:05:19 -05:00
Asier Isayas
b6d17284b5 Add MV Panel 2025-03-06 15:05:12 -05:00
Nishtha Ahuja
7b7a2817b6 All views associated with a container (#2063) and Materialized View Target Container (#2065)
Identified Source container and Target container
Created tabs in Scale and Settings respectively
Changed the Icon of target container
2025-03-06 19:01:58 +05:30
Laurent Nguyen
083bccfda9 Prepare for Fabric native (#2050)
* Implement fabric native path

* Fix default values to work with current fabric clients

* Fix Fabric native mode

* Fix unit test

* export Fabric context

* Dynamically close Home tab for Mirrored databases in Fabric rather than conditional init (which doesn't work for Native)

* For Fabric native, don't show "Delete Database" in context menu and reading databases should return the database from the context.

* Update to V3 messaging

* For data plane operations, skip ARM for Fabric native. Refine the tests for fabric to make the distinction between mirrored key, mirrored AAD and native. Fix FabricUtil to strict compile.

* Add support for refreshing access tokens

* Buf fix: don't wait for refresh is async

* Fix format

* Fix strict compile issue

---------

Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2025-03-06 07:30:13 +01:00
SATYA SB
14c9874e5e [accessibility-3560325]:[Programmatic access - Cosmos DB Query Copilot - Query Faster with Copilot>Enable Query Advisor]: Element's role present under 'Sample Query1' tab does not support its ARIA attributes. (#2059)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-02-25 13:35:59 +05:30
jawelton74
a04eaff6be Add Tables to missing api type checks for dataplane RBAC. (#2060)
* Add Tables to missing api type checks for dataplane RBAC.

* Comment out test that is broken due to invalid hook call error.
2025-02-20 08:15:53 -08:00
jawelton74
51a412e2c0 Change value of the example SelfServeType enum to match name of (#2062)
localization file.
2025-02-20 07:06:25 -08:00
Asier Isayas
f0e32491d7 Partition Key, Throughput, Unique Keys 2025-02-19 15:16:32 -05:00
SATYA SB
3fcbdf6152 [accessibility-3739790-3739677]:[Forms and Validation - Azure Cosmos DB- Data Explorer - New Vertex]: Visual Label is not defined for Key, Value and Type input fields under 'New Vertex' pane. (#2040)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-02-19 11:26:15 +05:30
SATYA SB
8da078579e [accessibility-3739618]:[Screen Reader - Azure Cosmos DB- Data Explorer - Graphs]: Screen Reader announces both expanded and collapsed information simultaneously for expand/collapse button in bottom notification region under 'Data Explorer' pane. (#2048)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-02-19 11:25:44 +05:30
vchske
4ac41031e6 Fixing SelfServeType enum to work in MPAC (#2057) 2025-02-18 09:59:51 -08:00
Asier Isayas
c33c497fd9 subpartition keys 2025-02-18 12:30:55 -05:00
jawelton74
d7923db108 Add Tables as an API type that supports dataplane RBAC. (#2056) 2025-02-18 09:29:53 -08:00
Asier Isayas
cec621443d undefined check 2025-02-14 14:58:12 -05:00
Asier Isayas
b8017763b7 AddMaterializedViewPanel 2025-02-14 14:56:19 -05:00
Asier Isayas
59619a856e fetch MV properties from RP API and capture them in our data models 2025-02-14 14:55:34 -05:00
SATYA SB
0170c9e1cc [accessibility-3739182]:[Visual Requirement - Azure Cosmos DB - Add Row]: Ensures the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds. (#2054)
Co-authored-by: Satyapriya Bai <v-satybai@microsoft.com>
2025-02-14 11:53:01 +05:30
Asier Isayas
b1f016a796 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into feature/materialized-views 2025-02-13 17:21:51 -05:00
Asier Isayas
ae3912cbf2 add Materialized Views feature flag 2025-02-13 17:21:33 -05:00
bogercraig
2730da7ab6 Backend Migration - Remove Use of Legacy Backend from DE (#2043)
* Default to new backend endpoint if the endpoint in current context does not match existing set in constants.

* Remove some env references.

* Added comments with reasoning for selecting new backend by default.

* Update comment.

* Remove all references to useNewPortalBackendEndpoint now that old backend is disabled in all environments.

* Resolve lint issues.

* Removed references to old backend from Cassandra and Mongo Apis

* fix unit tests

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-02-12 18:12:59 -08:00
sunghyunkang1111
de2449ee25 Adding throughput bucket settings in Data Explorer (#2044)
* Added throughput bucketing

* fix bugs

* enable/disable per autoscale selection

* Added logic

* change query bucket to group

* Updated to a tab

* Fixed unit tests

* Edit package-lock

* Compile build fix

* fix unit tests

* moving the throughput bucket flag to the client generation level
2025-02-12 13:10:07 -06:00
sunghyunkang1111
99378582ce Remove blocking await on sample database (#2047)
* Remove blocking await on sample database

* Remove compress flag to reduce bundle size

* Fix typo in webpack config comment date
2025-02-12 13:09:52 -06:00
121 changed files with 3914 additions and 1889 deletions

View File

@@ -61,6 +61,8 @@
@GalleryBackgroundColor: #fdfdfd; @GalleryBackgroundColor: #fdfdfd;
@LinkColor: #2d6da4;
//Icons //Icons
@InfoIconColor: #0072c6; @InfoIconColor: #0072c6;
@WarningIconColor: #db7500; @WarningIconColor: #db7500;
@@ -246,6 +248,10 @@
outline: 1px dashed @FocusColor; outline: 1px dashed @FocusColor;
} }
.focusedBorder() {
border: 1px dashed @FocusColor;
}
/************************************************************************************************ /************************************************************************************************
Common Toggle Switch Common Toggle Switch
*************************************************************************************************/ *************************************************************************************************/

View File

@@ -530,6 +530,13 @@ export class ariaLabelForLearnMoreLink {
public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link."; public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link.";
} }
export class MaterializedViewsLabels {
public static readonly NewMaterializedView: string = "New Materialized View";
}
export class FeedbackLabels {
public static readonly provideFeedback: string = "Provide feedback";
}
export const QueryCopilotSampleDatabaseId = "CopilotSampleDB"; export const QueryCopilotSampleDatabaseId = "CopilotSampleDB";
export const QueryCopilotSampleContainerId = "SampleContainer"; export const QueryCopilotSampleContainerId = "SampleContainer";

View File

@@ -1,14 +1,15 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens"; import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { AuthorizationToken } from "Contracts/FabricMessageTypes"; 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 { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BackendApi, PriorityLevel } from "../Common/Constants"; import { PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext"; 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 { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
@@ -19,7 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => { export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
const { verb, resourceId, resourceType, headers } = 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) { if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
Logger.logInfo( Logger.logInfo(
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `, `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); return decodeURIComponent(headers.authorization);
} }
if (configContext.platform === Platform.Fabric) { if (isFabricMirroredKey()) {
switch (requestInfo.resourceType) { switch (requestInfo.resourceType) {
case Cosmos.ResourceType.conflicts: case Cosmos.ResourceType.conflicts:
case Cosmos.ResourceType.container: case Cosmos.ResourceType.container:
@@ -54,8 +55,13 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
// User resource tokens // User resource tokens
// TODO userContext.fabricContext.databaseConnectionInfo can be undefined // TODO userContext.fabricContext.databaseConnectionInfo can be undefined
headers[HttpHeaders.msDate] = new Date().toUTCString(); headers[HttpHeaders.msDate] = new Date().toUTCString();
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens; const resourceTokens = (
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp); 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); return getAuthorizationTokenUsingResourceTokens(resourceTokens, requestInfo.path, requestInfo.resourceId);
case Cosmos.ResourceType.none: 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. // 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 // 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). // (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); return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
/* ************** TODO: Uncomment this code if we need to support these operations ************** /* ************** TODO: Uncomment this code if we need to support these operations **************
@@ -125,10 +133,6 @@ export async function getTokenFromAuthService(
resourceType: string, resourceType: string,
resourceId?: string, resourceId?: string,
): Promise<AuthorizationToken> { ): Promise<AuthorizationToken> {
if (!useNewPortalBackendEndpoint(BackendApi.RuntimeProxy)) {
return getTokenFromAuthService_ToBeDeprecated(verb, resourceType, resourceId);
}
try { try {
const host: string = configContext.PORTAL_BACKEND_ENDPOINT; const host: string = configContext.PORTAL_BACKEND_ENDPOINT;
const response: Response = await _global.fetch(host + "/api/connectionstring/runtimeproxy/authorizationtokens", { 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 // The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
enum SDKSupportedCapabilities { enum SDKSupportedCapabilities {
None = 0, None = 0,
@@ -203,8 +179,10 @@ export function client(): Cosmos.CosmosClient {
} }
let _defaultHeaders: Cosmos.CosmosHeaders = {}; let _defaultHeaders: Cosmos.CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] = _defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge; SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
_defaultHeaders["x-ms-cosmos-throughput-bucket"] = 1;
if ( if (
userContext.authType === AuthType.ConnectionString || userContext.authType === AuthType.ConnectionString ||

View File

@@ -26,3 +26,7 @@ export function getWorkloadType(): WorkloadType {
} }
return workloadType; return workloadType;
} }
export function isMaterializedViewsEnabled(): boolean {
return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews;
}

View File

@@ -4,16 +4,8 @@ import { configContext, resetConfigContext, updateConfigContext } from "../Confi
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { import { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
deleteDocument,
getEndpoint,
getFeatureEndpointOrDefault,
queryDocuments,
readDocument,
updateDocument,
} from "./MongoProxyClient";
const databaseId = "testDB"; const databaseId = "testDB";
@@ -196,20 +188,8 @@ describe("MongoProxyClient", () => {
expect.any(Object), 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(() => { beforeEach(() => {
resetConfigContext(); resetConfigContext();
updateUserContext({ updateUserContext({
@@ -226,9 +206,9 @@ describe("MongoProxyClient", () => {
}); });
it("builds the correct URL", () => { it("builds the correct URL", () => {
deleteDocument(databaseId, collection, documentId); deleteDocuments(databaseId, collection, [documentId]);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
expect.any(Object), expect.any(Object),
); );
}); });
@@ -238,9 +218,9 @@ describe("MongoProxyClient", () => {
MONGO_PROXY_ENDPOINT: "https://localhost:1234", MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [], globallyEnabledMongoAPIs: [],
}); });
deleteDocument(databaseId, collection, documentId); deleteDocuments(databaseId, collection, [documentId]);
expect(window.fetch).toHaveBeenCalledWith( expect(window.fetch).toHaveBeenCalledWith(
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`, `${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
expect.any(Object), expect.any(Object),
); );
}); });
@@ -275,33 +255,4 @@ describe("MongoProxyClient", () => {
expect(endpoint).toEqual(`${configContext.MONGO_PROXY_ENDPOINT}/api/connectionstring/mongo/explorer`); 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`);
});
});
}); });

View File

@@ -1,20 +1,13 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import {
allowedMongoProxyEndpoints_ToBeDeprecated,
defaultAllowedMongoProxyEndpoints,
validateEndpoint,
} from "Utils/EndpointUtils";
import queryString from "querystring";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants"; import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
@@ -67,10 +60,6 @@ export function queryDocuments(
query: string, query: string,
continuationToken?: string, continuationToken?: string,
): Promise<QueryResponse> { ): Promise<QueryResponse> {
if (!useMongoProxyEndpoint(MongoProxyApi.ResourceList) || !useMongoProxyEndpoint(MongoProxyApi.QueryDocuments)) {
return queryDocuments_ToBeDeprecated(databaseId, collection, isResourceList, query, continuationToken);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = { const params = {
@@ -89,7 +78,7 @@ export function queryDocuments(
query, query,
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || ""; const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
const headers = { const headers = {
...defaultHeaders, ...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( export function readDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
documentId: DocumentId, documentId: DocumentId,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.ReadDocument)) {
return readDocument_ToBeDeprecated(databaseId, collection, documentId);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
@@ -217,7 +141,7 @@ export function readDocument(
: "", : "",
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument); const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
return window return window
.fetch(endpoint, { .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( export function createDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
partitionKeyProperty: string, partitionKeyProperty: string,
documentContent: unknown, documentContent: unknown,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.CreateDocument)) {
return createDocument_ToBeDeprecated(databaseId, collection, partitionKeyProperty, documentContent);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const params = { const params = {
@@ -308,7 +183,7 @@ export function createDocument(
documentContent: JSON.stringify(documentContent), documentContent: JSON.stringify(documentContent),
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument); const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
return window return window
.fetch(`${endpoint}/createDocument`, { .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( export function updateDocument(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
documentId: DocumentId, documentId: DocumentId,
documentContent: string, documentContent: string,
): Promise<DataModels.DocumentId> { ): Promise<DataModels.DocumentId> {
if (!useMongoProxyEndpoint(MongoProxyApi.UpdateDocument)) {
return updateDocument_ToBeDeprecated(databaseId, collection, documentId, documentContent);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint; const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
const idComponents = documentId.self.split("/"); const idComponents = documentId.self.split("/");
@@ -396,7 +229,7 @@ export function updateDocument(
: "", : "",
documentContent, documentContent,
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument); const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
return window return window
.fetch(endpoint, { .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( export function deleteDocuments(
databaseId: string, databaseId: string,
collection: Collection, collection: Collection,
@@ -575,7 +275,7 @@ export function deleteDocuments(
resourceGroup: userContext.resourceGroup, resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name, databaseAccountName: databaseAccount.name,
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete); const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
return window return window
.fetch(`${endpoint}/bulkdelete`, { .fetch(`${endpoint}/bulkdelete`, {
@@ -599,9 +299,6 @@ export function deleteDocuments(
export function createMongoCollectionWithProxy( export function createMongoCollectionWithProxy(
params: DataModels.CreateCollectionParams, params: DataModels.CreateCollectionParams,
): Promise<DataModels.Collection> { ): Promise<DataModels.Collection> {
if (!useMongoProxyEndpoint(MongoProxyApi.CreateCollectionWithProxy)) {
return createMongoCollectionWithProxy_ToBeDeprecated(params);
}
const { databaseAccount } = userContext; const { databaseAccount } = userContext;
const shardKey: string = params.partitionKey?.paths[0]; const shardKey: string = params.partitionKey?.paths[0];
@@ -622,7 +319,7 @@ export function createMongoCollectionWithProxy(
isSharded: !!shardKey, isSharded: !!shardKey,
}; };
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy); const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
return window return window
.fetch(`${endpoint}/createCollection`, { .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 { export function getEndpoint(endpoint: string): string {
let url = endpoint + "/api/mongo/explorer"; let url = endpoint + "/api/mongo/explorer";
@@ -719,84 +352,6 @@ export function getEndpoint(endpoint: string): string {
return url; 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 { export class ThrottlingError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);

View File

@@ -1,10 +1,10 @@
import { Platform, configContext } from "../ConfigContext"; import { isFabric } from "Platform/Fabric/FabricUtil";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less"); export const StyleConstants = require("less-vars-loader!../../less/Common/Constants.less");
export function updateStyles(): void { export function updateStyles(): void {
if (configContext.platform === Platform.Fabric) { if (isFabric()) {
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh; StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium; StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
StyleConstants.AccentLight = StyleConstants.FabricAccentLight; StyleConstants.AccentLight = StyleConstants.FabricAccentLight;

View File

@@ -1,4 +1,5 @@
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos"; import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases"; import { useDatabases } from "../../Explorer/useDatabases";
@@ -24,7 +25,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
); );
try { try {
let collection: DataModels.Collection; let collection: DataModels.Collection;
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
if (params.createNewDatabase) { if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput, autoPilotMaxThroughput: params.autoPilotMaxThroughput,

View File

@@ -0,0 +1,70 @@
import { constructRpOptions } from "Common/dataAccess/createCollection";
import { handleError } from "Common/ErrorHandlingUtils";
import { Collection, CreateMaterializedViewsParams } from "Contracts/DataModels";
import { userContext } from "UserContext";
import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources";
import {
CreateUpdateOptions,
SqlContainerResource,
SqlDatabaseCreateUpdateParameters,
} from "Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
export const createMaterializedView = async (params: CreateMaterializedViewsParams): Promise<Collection> => {
const clearMessage = logConsoleProgress(
`Creating a new materialized view ${params.materializedViewId} for database ${params.databaseId}`,
);
const options: CreateUpdateOptions = constructRpOptions(params);
const resource: SqlContainerResource = {
id: params.materializedViewId,
};
if (params.materializedViewDefinition) {
resource.materializedViewDefinition = params.materializedViewDefinition;
}
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
if (params.fullTextPolicy) {
resource.fullTextPolicy = params.fullTextPolicy;
}
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource,
options,
},
};
try {
const createResponse = await createUpdateSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.materializedViewId,
rpPayload,
);
logConsoleInfo(`Successfully created materialized view ${params.materializedViewId}`);
return createResponse && (createResponse.properties.resource as Collection);
} catch (error) {
handleError(error, "CreateMaterializedView", `Error while creating materialized view ${params.materializedViewId}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,3 +1,4 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; 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> { export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
await deleteCollectionWithARM(databaseId, collectionId); await deleteCollectionWithARM(databaseId, collectionId);
} else { } else {
await client().database(databaseId).container(collectionId).delete(); await client().database(databaseId).container(collectionId).delete();

View File

@@ -105,6 +105,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
? parseInt(resource.softAllowedMaximumThroughput) ? parseInt(resource.softAllowedMaximumThroughput)
: resource.softAllowedMaximumThroughput; : resource.softAllowedMaximumThroughput;
const throughputBuckets = resource?.throughputBuckets;
if (autoscaleSettings) { if (autoscaleSettings) {
return { return {
id: offerId, id: offerId,
@@ -114,6 +116,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
offerReplacePending: resource.offerReplacePending === "true", offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput, instantMaximumThroughput,
softAllowedMaximumThroughput, softAllowedMaximumThroughput,
throughputBuckets,
}; };
} }
@@ -125,6 +128,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
offerReplacePending: resource.offerReplacePending === "true", offerReplacePending: resource.offerReplacePending === "true",
instantMaximumThroughput, instantMaximumThroughput,
softAllowedMaximumThroughput, softAllowedMaximumThroughput,
throughputBuckets,
}; };
} }

View File

@@ -1,9 +1,10 @@
import { ContainerResponse } from "@azure/cosmos"; import { ContainerResponse } from "@azure/cosmos";
import { Queries } from "Common/Constants"; 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 { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { FabricArtifactInfo, userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; 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[]> { export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`); const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
if ( if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
configContext.platform === Platform.Fabric &&
userContext.fabricContext &&
userContext.fabricContext.databaseConnectionInfo.databaseId === databaseId
) {
const collections: DataModels.Collection[] = []; const collections: DataModels.Collection[] = [];
const promises: Promise<ContainerResponse>[] = []; const promises: Promise<ContainerResponse>[] = [];
for (const collectionResourceId in userContext.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 // Dictionary key looks like this: dbs/SampleDB/colls/Container
const resourceIdObj = collectionResourceId.split("/"); const resourceIdObj = collectionResourceId.split("/");
const tokenDatabaseId = resourceIdObj[1]; const tokenDatabaseId = resourceIdObj[1];
@@ -56,7 +55,8 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
return await readCollectionsWithARM(databaseId); return await readCollectionsWithARM(databaseId);
} }
@@ -126,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
throw new Error(`Unsupported default experience type: ${apiType}`); throw new Error(`Unsupported default experience type: ${apiType}`);
} }
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection); // TO DO: Remove when we get RP API Spec with materializedViews
/* eslint-disable @typescript-eslint/no-explicit-any */
return rpResponse?.value?.map((collection: any) => {
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
return collectionDataModel;
});
} }

View File

@@ -1,4 +1,4 @@
import { Platform, configContext } from "ConfigContext"; import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels"; import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -11,8 +11,9 @@ import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK"; import { readOfferWithSDK } from "./readOfferWithSDK";
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => { export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
if (configContext.platform === Platform.Fabric) { if (isFabricMirroredKey() || isFabricNative()) {
// TODO This works, but is very slow, because it requests the token, so we skip for now // 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"); console.error("Skiping readDatabaseOffer for Fabric");
return undefined; return undefined;
} }
@@ -23,7 +24,8 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
return await readDatabaseOfferWithARM(params.databaseId); return await readDatabaseOfferWithARM(params.databaseId);
} }

View File

@@ -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 { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { FabricArtifactInfo, userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
@@ -14,8 +15,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[]; let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`); const clearMessage = logConsoleProgress(`Querying databases`);
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) { if (
const tokensData = userContext.fabricContext.databaseConnectionInfo; 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 const databaseIdsSet = new Set<string>(); // databaseId
@@ -46,13 +52,28 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
})); }));
clearMessage(); clearMessage();
return databases; 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 { try {
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
databases = await readDatabasesWithARM(); databases = await readDatabasesWithARM();
} else { } else {

View File

@@ -1,4 +1,5 @@
import { ContainerDefinition, RequestOptions } from "@azure/cosmos"; import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels"; import { Collection } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -36,7 +37,8 @@ export async function updateCollection(
if ( if (
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations && !userContext.features.enableSDKoperations &&
userContext.apiType !== "Tables" userContext.apiType !== "Tables" &&
!isFabric()
) { ) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection); collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else { } else {

View File

@@ -1,6 +1,7 @@
import { OfferDefinition, RequestOptions } from "@azure/cosmos"; import { OfferDefinition, RequestOptions } from "@azure/cosmos";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels"; import { Offer, SDKOfferDefinition, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { import {
migrateCassandraKeyspaceToAutoscale, migrateCassandraKeyspaceToAutoscale,
@@ -56,7 +57,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`); const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
try { try {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) { if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations && !isFabric()) {
if (params.collectionId) { if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params); updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.apiType === "Tables") { } else if (userContext.apiType === "Tables") {
@@ -359,6 +360,13 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
body.properties.resource.throughput = params.manualThroughput; 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; return body;
}; };

View File

@@ -12,7 +12,6 @@ import {
allowedGraphEndpoints, allowedGraphEndpoints,
allowedHostedExplorerEndpoints, allowedHostedExplorerEndpoints,
allowedJunoOrigins, allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints, allowedMsalRedirectEndpoints,
defaultAllowedArmEndpoints, defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints, defaultAllowedBackendEndpoints,
@@ -50,10 +49,8 @@ export interface ConfigContext {
CATALOG_API_KEY: string; CATALOG_API_KEY: string;
ARCADIA_ENDPOINT: string; ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string; ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string;
PORTAL_BACKEND_ENDPOINT: string; PORTAL_BACKEND_ENDPOINT: string;
NEW_BACKEND_APIS?: BackendApi[]; NEW_BACKEND_APIS?: BackendApi[];
MONGO_BACKEND_ENDPOINT?: string;
MONGO_PROXY_ENDPOINT: string; MONGO_PROXY_ENDPOINT: string;
CASSANDRA_PROXY_ENDPOINT: string; CASSANDRA_PROXY_ENDPOINT: string;
NEW_CASSANDRA_APIS?: 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_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 GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: JunoEndpoints.Prod, JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
@@ -152,15 +148,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT; delete newContext.ARCADIA_ENDPOINT;
} }
if (
!validateEndpoint(
newContext.BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
)
) {
delete newContext.BACKEND_ENDPOINT;
}
if ( if (
!validateEndpoint( !validateEndpoint(
newContext.MONGO_PROXY_ENDPOINT, newContext.MONGO_PROXY_ENDPOINT,
@@ -170,10 +157,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.MONGO_PROXY_ENDPOINT; delete newContext.MONGO_PROXY_ENDPOINT;
} }
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
delete newContext.MONGO_BACKEND_ENDPOINT;
}
if ( if (
!validateEndpoint( !validateEndpoint(
newContext.CASSANDRA_PROXY_ENDPOINT, newContext.CASSANDRA_PROXY_ENDPOINT,

View File

@@ -32,6 +32,7 @@ export interface DatabaseAccountExtendedProperties {
writeLocations?: DatabaseAccountResponseLocation[]; writeLocations?: DatabaseAccountResponseLocation[];
enableFreeTier?: boolean; enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean; enableAnalyticalStorage?: boolean;
enableMaterializedViews?: boolean;
isVirtualNetworkFilterEnabled?: boolean; isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[]; ipRules?: IpRule[];
privateEndpointConnections?: unknown[]; privateEndpointConnections?: unknown[];
@@ -164,6 +165,8 @@ export interface Collection extends Resource {
schema?: ISchema; schema?: ISchema;
requestSchema?: () => void; requestSchema?: () => void;
computedProperties?: ComputedProperties; computedProperties?: ComputedProperties;
materializedViews?: MaterializedView[];
materializedViewDefinition?: MaterializedViewDefinition;
} }
export interface CollectionsWithPagination { export interface CollectionsWithPagination {
@@ -223,6 +226,17 @@ export interface ComputedProperty {
export type ComputedProperties = ComputedProperty[]; export type ComputedProperties = ComputedProperty[];
export interface MaterializedView {
id: string;
_rid: string;
}
export interface MaterializedViewDefinition {
definition: string;
sourceCollectionId: string;
sourceCollectionRid?: string;
}
export interface PartitionKey { export interface PartitionKey {
paths: string[]; paths: string[];
kind: "Hash" | "Range" | "MultiHash"; kind: "Hash" | "Range" | "MultiHash";
@@ -275,6 +289,12 @@ export interface Offer {
offerReplacePending: boolean; offerReplacePending: boolean;
instantMaximumThroughput?: number; instantMaximumThroughput?: number;
softAllowedMaximumThroughput?: number; softAllowedMaximumThroughput?: number;
throughputBuckets?: ThroughputBucket[];
}
export interface ThroughputBucket {
id: number;
maxThroughputPercentage: number;
} }
export interface SDKOfferDefinition extends Resource { export interface SDKOfferDefinition extends Resource {
@@ -339,9 +359,7 @@ export interface CreateDatabaseParams {
offerThroughput?: number; offerThroughput?: number;
} }
export interface CreateCollectionParams { export interface CreateCollectionParamsBase {
createNewDatabase: boolean;
collectionId: string;
databaseId: string; databaseId: string;
databaseLevelThroughput: boolean; databaseLevelThroughput: boolean;
offerThroughput?: number; offerThroughput?: number;
@@ -355,6 +373,16 @@ export interface CreateCollectionParams {
fullTextPolicy?: FullTextPolicy; fullTextPolicy?: FullTextPolicy;
} }
export interface CreateCollectionParams extends CreateCollectionParamsBase {
createNewDatabase: boolean;
collectionId: string;
}
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
materializedViewId: string;
materializedViewDefinition: MaterializedViewDefinition;
}
export interface VectorEmbeddingPolicy { export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[]; vectorEmbeddings: VectorEmbedding[];
} }
@@ -397,6 +425,7 @@ export interface UpdateOfferParams {
collectionId?: string; collectionId?: string;
migrateToAutoPilot?: boolean; migrateToAutoPilot?: boolean;
migrateToManual?: boolean; migrateToManual?: boolean;
throughputBuckets?: ThroughputBucket[];
} }
export interface Notification { export interface Notification {

View File

@@ -4,6 +4,7 @@
export enum FabricMessageTypes { export enum FabricMessageTypes {
GetAuthorizationToken = "GetAuthorizationToken", GetAuthorizationToken = "GetAuthorizationToken",
GetAllResourceTokens = "GetAllResourceTokens", GetAllResourceTokens = "GetAllResourceTokens",
GetAccessToken = "GetAccessToken",
Ready = "Ready", Ready = "Ready",
} }

View File

@@ -1,47 +1,9 @@
import { AuthorizationToken } from "Contracts/FabricMessageTypes"; import { AuthorizationToken } from "./FabricMessageTypes";
// This is the version of these messages // This is the version of these messages
export const FABRIC_RPC_VERSION = "2"; export const FABRIC_RPC_VERSION = "FabricMessageV3";
// Fabric to Data Explorer // 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 = export type FabricMessageV2 =
| { | {
type: "newContainer"; type: "newContainer";
@@ -69,7 +31,7 @@ export type FabricMessageV2 =
message: { message: {
id: string; id: string;
error: string | undefined; error: string | undefined;
data: FabricDatabaseConnectionInfo | undefined; data: ResourceTokenInfo | undefined;
}; };
} }
| { | {
@@ -79,17 +41,81 @@ export type FabricMessageV2 =
}; };
}; };
export type CosmosDBTokenResponse = { export type FabricMessageV3 =
token: string; | {
date: string; 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; endpoint: string;
databaseId: 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; resourceTokensTimestamp: number;
} }

View File

@@ -143,6 +143,8 @@ export interface Collection extends CollectionBase {
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>; geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
documentIds: ko.ObservableArray<DocumentId>; documentIds: ko.ObservableArray<DocumentId>;
computedProperties: ko.Observable<DataModels.ComputedProperties>; computedProperties: ko.Observable<DataModels.ComputedProperties>;
materializedViews: ko.Observable<DataModels.MaterializedView[]>;
materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
cassandraKeys: CassandraTableKeys; cassandraKeys: CassandraTableKeys;
cassandraSchema: CassandraTableKey[]; cassandraSchema: CassandraTableKey[];
@@ -406,7 +408,6 @@ export interface DataExplorerInputsFrame {
csmEndpoint?: string; csmEndpoint?: string;
dnsSuffix?: string; dnsSuffix?: string;
serverId?: string; serverId?: string;
extensionEndpoint?: string;
portalBackendEndpoint?: string; portalBackendEndpoint?: string;
mongoProxyEndpoint?: string; mongoProxyEndpoint?: string;
cassandraProxyEndpoint?: string; cassandraProxyEndpoint?: string;

View File

@@ -1,5 +1,13 @@
import { MaterializedViewsLabels } from "Common/Constants";
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import {
AddMaterializedViewPanel,
AddMaterializedViewPanelProps,
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -19,7 +27,6 @@ import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { Platform, configContext } from "./../ConfigContext";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook"; import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
@@ -41,7 +48,7 @@ export interface DatabaseContextMenuButtonParams {
* New resource tree (in ReactJS) * New resource tree (in ReactJS)
*/ */
export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => { export const createDatabaseContextMenu = (container: Explorer, databaseId: string): TreeNodeMenuItem[] => {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { if (isFabric() && userContext.fabricContext?.isReadOnly) {
return undefined; return undefined;
} }
@@ -53,7 +60,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({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
@@ -145,7 +152,7 @@ export const createCollectionContextMenuButton = (
}); });
} }
if (configContext.platform !== Platform.Fabric) { if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
items.push({ items.push({
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
@@ -163,6 +170,24 @@ export const createCollectionContextMenuButton = (
}); });
} }
if (isMaterializedViewsEnabled() && !selectedCollection.materializedViewDefinition()) {
items.push({
label: MaterializedViewsLabels.NewMaterializedView,
onClick: () => {
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
explorer: container,
sourceContainer: selectedCollection,
};
useSidePanel
.getState()
.openSidePanel(
MaterializedViewsLabels.NewMaterializedView,
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
);
},
});
}
return items; return items;
}; };

View File

@@ -1,5 +1,7 @@
import { AuthType } from "AuthType";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import ko from "knockout"; import ko from "knockout";
import { Features } from "Platform/Hosted/extractFeatures";
import React from "react"; import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
@@ -247,4 +249,42 @@ describe("SettingsComponent", () => {
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom); expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath); 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);
});
}); });

View File

@@ -7,6 +7,10 @@ import {
ContainerPolicyComponent, ContainerPolicyComponent,
ContainerPolicyComponentProps, ContainerPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/ContainerPolicyComponent";
import {
ThroughputBucketsComponent,
ThroughputBucketsComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils";
@@ -41,6 +45,10 @@ import {
ConflictResolutionComponentProps, ConflictResolutionComponentProps,
} from "./SettingsSubComponents/ConflictResolutionComponent"; } from "./SettingsSubComponents/ConflictResolutionComponent";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import {
MaterializedViewComponent,
MaterializedViewComponentProps,
} from "./SettingsSubComponents/MaterializedViewComponent";
import { import {
MongoIndexingPolicyComponent, MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps, MongoIndexingPolicyComponentProps,
@@ -86,6 +94,8 @@ export interface SettingsComponentState {
wasAutopilotOriginallySet: boolean; wasAutopilotOriginallySet: boolean;
isScaleSaveable: boolean; isScaleSaveable: boolean;
isScaleDiscardable: boolean; isScaleDiscardable: boolean;
throughputBuckets: DataModels.ThroughputBucket[];
throughputBucketsBaseline: DataModels.ThroughputBucket[];
throughputError: string; throughputError: string;
timeToLive: TtlType; timeToLive: TtlType;
@@ -104,6 +114,7 @@ export interface SettingsComponentState {
changeFeedPolicyBaseline: ChangeFeedPolicyState; changeFeedPolicyBaseline: ChangeFeedPolicyState;
isSubSettingsSaveable: boolean; isSubSettingsSaveable: boolean;
isSubSettingsDiscardable: boolean; isSubSettingsDiscardable: boolean;
isThroughputBucketsSaveable: boolean;
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy; vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy; vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
@@ -155,9 +166,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean; private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean; private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean; private shouldShowPartitionKeyEditor: boolean;
private isMaterializedView: boolean;
private isVectorSearchEnabled: boolean; private isVectorSearchEnabled: boolean;
private isFullTextSearchEnabled: boolean; private isFullTextSearchEnabled: boolean;
private totalThroughputUsed: number; private totalThroughputUsed: number;
private throughputBucketsEnabled: boolean;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
constructor(props: SettingsComponentProps) { constructor(props: SettingsComponentProps) {
@@ -171,10 +184,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL"; this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo"; this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud(); this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isMaterializedView =
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection); this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection); this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; 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" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
@@ -193,6 +212,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
wasAutopilotOriginallySet: false, wasAutopilotOriginallySet: false,
isScaleSaveable: false, isScaleSaveable: false,
isScaleDiscardable: false, isScaleDiscardable: false,
throughputBuckets: undefined,
throughputBucketsBaseline: undefined,
throughputError: undefined, throughputError: undefined,
timeToLive: undefined, timeToLive: undefined,
@@ -211,6 +232,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
changeFeedPolicyBaseline: undefined, changeFeedPolicyBaseline: undefined,
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
isThroughputBucketsSaveable: false,
vectorEmbeddingPolicy: undefined, vectorEmbeddingPolicy: undefined,
vectorEmbeddingPolicyBaseline: undefined, vectorEmbeddingPolicyBaseline: undefined,
@@ -327,7 +349,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty || this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) (!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable) ||
this.state.isThroughputBucketsSaveable
); );
}; };
@@ -339,7 +362,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isIndexingPolicyDirty || this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty || this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty || this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) (!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable) ||
this.state.isThroughputBucketsSaveable
); );
}; };
@@ -419,6 +443,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ this.setState({
throughput: this.state.throughputBaseline, throughput: this.state.throughputBaseline,
throughputBuckets: this.state.throughputBucketsBaseline,
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
timeToLive: this.state.timeToLiveBaseline, timeToLive: this.state.timeToLiveBaseline,
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline, timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
displayedTtlSeconds: this.state.displayedTtlSecondsBaseline, displayedTtlSeconds: this.state.displayedTtlSecondsBaseline,
@@ -441,6 +467,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isScaleSaveable: false, isScaleSaveable: false,
isScaleDiscardable: false, isScaleDiscardable: false,
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isThroughputBucketsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
isContainerPolicyDirty: false, isContainerPolicyDirty: false,
isIndexingPolicyDirty: false, isIndexingPolicyDirty: false,
@@ -479,6 +506,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void => private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
this.setState({ indexingPolicyContent: newIndexingPolicy }); this.setState({ indexingPolicyContent: newIndexingPolicy });
private onThroughputBucketsSaveableChange = (isSaveable: boolean): void => {
this.setState({ isThroughputBucketsSaveable: isSaveable });
};
private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false }); private resetShouldDiscardContainerPolicies = (): void => this.setState({ shouldDiscardContainerPolicies: false });
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false }); private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
@@ -749,9 +780,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
] as DataModels.ComputedProperties; ] as DataModels.ComputedProperties;
} }
const throughputBuckets = this.offer?.throughputBuckets;
return { return {
throughput: offerThroughput, throughput: offerThroughput,
throughputBaseline: offerThroughput, throughputBaseline: offerThroughput,
throughputBuckets,
throughputBucketsBaseline: throughputBuckets,
changeFeedPolicy: changeFeedPolicy, changeFeedPolicy: changeFeedPolicy,
changeFeedPolicyBaseline: changeFeedPolicy, changeFeedPolicyBaseline: changeFeedPolicy,
timeToLive: timeToLive, timeToLive: timeToLive,
@@ -839,6 +874,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ throughput: newThroughput, throughputError }); this.setState({ throughput: newThroughput, throughputError });
}; };
private onThroughputBucketChange = (throughputBuckets: DataModels.ThroughputBucket[]): void => {
this.setState({ throughputBuckets });
};
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void => private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
this.setState({ isAutoPilotSelected: isAutoPilotSelected }); this.setState({ isAutoPilotSelected: isAutoPilotSelected });
@@ -1029,6 +1068,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) { if (this.state.isScaleSaveable) {
const updateOfferParams: DataModels.UpdateOfferParams = { const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId, databaseId: this.collection.databaseId,
@@ -1209,12 +1266,24 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange, onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
}; };
const throughputBucketsComponentProps: ThroughputBucketsComponentProps = {
currentBuckets: this.state.throughputBuckets,
throughputBucketsBaseline: this.state.throughputBucketsBaseline,
onBucketsChange: this.onThroughputBucketChange,
onSaveableChange: this.onThroughputBucketsSaveableChange,
};
const partitionKeyComponentProps: PartitionKeyComponentProps = { const partitionKeyComponentProps: PartitionKeyComponentProps = {
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId), database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
collection: this.collection, collection: this.collection,
explorer: this.props.settingsTab.getContainer(), explorer: this.props.settingsTab.getContainer(),
}; };
const materializedViewComponentProps: MaterializedViewComponentProps = {
collection: this.collection,
explorer: this.props.settingsTab.getContainer(),
};
const tabs: SettingsV2TabInfo[] = []; const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) { if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({ tabs.push({
@@ -1271,6 +1340,20 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}); });
} }
if (this.throughputBucketsEnabled) {
tabs.push({
tab: SettingsV2TabTypes.ThroughputBucketsTab,
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
});
}
if (this.isMaterializedView) {
tabs.push({
tab: SettingsV2TabTypes.MaterializedViewTab,
content: <MaterializedViewComponent {...materializedViewComponentProps} />,
});
}
const pivotProps: IPivotProps = { const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange, onLinkClick: this.onPivotChange,
selectedKey: SettingsV2TabTypes[this.state.selectedTab], selectedKey: SettingsV2TabTypes[this.state.selectedTab],

View File

@@ -0,0 +1,46 @@
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { MaterializedViewComponent } from "./MaterializedViewComponent";
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
describe("MaterializedViewComponent", () => {
let testCollection: typeof collection;
let testExplorer: typeof container;
beforeEach(() => {
testCollection = { ...collection };
});
it("renders only the source component when materializedViewDefinition is missing", () => {
testCollection.materializedViews([
{ id: "view1", _rid: "rid1" },
{ id: "view2", _rid: "rid2" },
]);
testCollection.materializedViewDefinition(null);
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(true);
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
});
it("renders only the target component when materializedViews is missing", () => {
testCollection.materializedViews(null);
testCollection.materializedViewDefinition({
definition: "SELECT * FROM c WHERE c.id = 1",
sourceCollectionId: "source1",
sourceCollectionRid: "rid123",
});
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(true);
});
it("renders neither component when both are missing", () => {
testCollection.materializedViews(null);
testCollection.materializedViewDefinition(null);
const wrapper = shallow(<MaterializedViewComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(MaterializedViewSourceComponent).exists()).toBe(false);
expect(wrapper.find(MaterializedViewTargetComponent).exists()).toBe(false);
});
});

View File

@@ -0,0 +1,36 @@
import { FontIcon, Link, Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
export interface MaterializedViewComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const MaterializedViewComponent: React.FC<MaterializedViewComponentProps> = ({ collection, explorer }) => {
const isTargetContainer = !!collection?.materializedViewDefinition();
const isSourceContainer = !!collection?.materializedViews();
return (
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following views defined for it.</Text>
<Text>
<Link
target="_blank"
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
>
Learn more
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
</Link>{" "}
about how to define materialized views and how to use them.
</Text>
</Stack>
{isSourceContainer && <MaterializedViewSourceComponent collection={collection} explorer={explorer} />}
{isTargetContainer && <MaterializedViewTargetComponent collection={collection} />}
</Stack>
);
};

View File

@@ -0,0 +1,36 @@
import { PrimaryButton } from "@fluentui/react";
import { shallow } from "enzyme";
import React from "react";
import { collection, container } from "../TestUtils";
import { MaterializedViewSourceComponent } from "./MaterializedViewSourceComponent";
describe("MaterializedViewSourceComponent", () => {
let testCollection: typeof collection;
let testExplorer: typeof container;
beforeEach(() => {
testCollection = { ...collection };
});
it("renders without crashing", () => {
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.exists()).toBe(true);
});
it("renders the PrimaryButton", () => {
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
});
it("updates when new materialized views are provided", () => {
const wrapper = shallow(<MaterializedViewSourceComponent collection={testCollection} explorer={testExplorer} />);
// Simulating an update by modifying the observable directly
testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]);
wrapper.setProps({ collection: testCollection });
wrapper.update();
expect(wrapper.find(PrimaryButton).exists()).toBe(true);
});
});

View File

@@ -0,0 +1,113 @@
import { PrimaryButton } from "@fluentui/react";
import { MaterializedViewsLabels } from "Common/Constants";
import Explorer from "Explorer/Explorer";
import { loadMonaco } from "Explorer/LazyMonaco";
import { AddMaterializedViewPanel } from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import * as monaco from "monaco-editor";
import React, { useEffect, useRef } from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
export interface MaterializedViewSourceComponentProps {
collection: ViewModels.Collection;
explorer: Explorer;
}
export const MaterializedViewSourceComponent: React.FC<MaterializedViewSourceComponentProps> = ({
collection,
explorer,
}) => {
const editorContainerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(null);
const materializedViews = collection?.materializedViews() ?? [];
// Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedViews[] with collection id.
const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => {
let definition = "";
let partitionKey: string[] = [];
useDatabases.getState().databases.find((database) => {
const collection = database.collections().find((collection) => collection.id() === viewId);
if (collection) {
const materializedViewDefinition = collection.materializedViewDefinition();
materializedViewDefinition && (definition = materializedViewDefinition.definition);
collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths);
}
});
return { definition, partitionKey };
};
//JSON value for the editor using the fetched id and definitions.
const jsonValue = JSON.stringify(
materializedViews.map((view) => {
const { definition, partitionKey } = getViewDetails(view.id);
return {
name: view.id,
partitionKey: partitionKey.join(", "),
definition,
};
}),
null,
2,
);
// Initialize Monaco editor with the computed JSON value.
useEffect(() => {
let disposed = false;
const initMonaco = async () => {
const monacoInstance = await loadMonaco();
if (disposed || !editorContainerRef.current) {
return;
}
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
value: jsonValue,
language: "json",
ariaLabel: "Materialized Views JSON",
readOnly: true,
});
};
initMonaco();
return () => {
disposed = true;
editorRef.current?.dispose();
};
}, [jsonValue]);
// Update the editor when the jsonValue changes.
useEffect(() => {
if (editorRef.current) {
editorRef.current.setValue(jsonValue);
}
}, [jsonValue]);
return (
<div>
<div
ref={editorContainerRef}
style={{
height: 250,
border: "1px solid #ccc",
borderRadius: 4,
overflow: "hidden",
}}
/>
<PrimaryButton
text="Add view"
styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() =>
useSidePanel
.getState()
.openSidePanel(
MaterializedViewsLabels.NewMaterializedView,
<AddMaterializedViewPanel explorer={explorer} sourceContainer={collection} />,
)
}
/>
</div>
);
};

View File

@@ -0,0 +1,32 @@
import { Text } from "@fluentui/react";
import { Collection } from "Contracts/ViewModels";
import { shallow } from "enzyme";
import React from "react";
import { collection } from "../TestUtils";
import { MaterializedViewTargetComponent } from "./MaterializedViewTargetComponent";
describe("MaterializedViewTargetComponent", () => {
let testCollection: Collection;
beforeEach(() => {
testCollection = {
...collection,
materializedViewDefinition: collection.materializedViewDefinition,
};
});
it("renders without crashing", () => {
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
expect(wrapper.exists()).toBe(true);
});
it("displays the source container ID", () => {
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(2).dive().text()).toBe("source1");
});
it("displays the materialized view definition", () => {
const wrapper = shallow(<MaterializedViewTargetComponent collection={testCollection} />);
expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1");
});
});

View File

@@ -0,0 +1,43 @@
import { Stack, Text } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
export interface MaterializedViewTargetComponentProps {
collection: ViewModels.Collection;
}
export const MaterializedViewTargetComponent: React.FC<MaterializedViewTargetComponentProps> = ({ collection }) => {
const materializedViewDefinition = collection?.materializedViewDefinition();
const textHeadingStyle = {
root: { fontWeight: "600", fontSize: 16 },
};
const valueBoxStyle = {
root: {
backgroundColor: "#f3f3f3",
padding: "5px 10px",
borderRadius: "4px",
},
};
return (
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
<Text styles={textHeadingStyle}>Materialized View Settings</Text>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
<Stack styles={valueBoxStyle}>
<Text>{materializedViewDefinition?.sourceCollectionId}</Text>
</Stack>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Materialized view definition</Text>
<Stack styles={valueBoxStyle}>
<Text>{materializedViewDefinition?.definition}</Text>
</Stack>
</Stack>
</Stack>
);
};

View File

@@ -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);
});
});

View File

@@ -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>
);
};

View File

@@ -11,7 +11,8 @@ export type isDirtyTypes =
| DataModels.IndexingPolicy | DataModels.IndexingPolicy
| DataModels.ComputedProperties | DataModels.ComputedProperties
| DataModels.VectorEmbedding[] | DataModels.VectorEmbedding[]
| DataModels.FullTextPolicy; | DataModels.FullTextPolicy
| DataModels.ThroughputBucket[];
export const TtlOff = "off"; export const TtlOff = "off";
export const TtlOn = "on"; export const TtlOn = "on";
export const TtlOnNoDefault = "on-nodefault"; export const TtlOnNoDefault = "on-nodefault";
@@ -55,6 +56,8 @@ export enum SettingsV2TabTypes {
PartitionKeyTab, PartitionKeyTab,
ComputedPropertiesTab, ComputedPropertiesTab,
ContainerVectorPolicyTab, ContainerVectorPolicyTab,
ThroughputBucketsTab,
MaterializedViewTab,
} }
export enum ContainerPolicyTabTypes { export enum ContainerPolicyTabTypes {
@@ -167,6 +170,10 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
return "Computed Properties"; return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab: case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Policies"; return "Container Policies";
case SettingsV2TabTypes.ThroughputBucketsTab:
return "Throughput Buckets";
case SettingsV2TabTypes.MaterializedViewTab:
return "Materialized Views (Preview)";
default: default:
throw new Error(`Unknown tab ${tab}`); throw new Error(`Unknown tab ${tab}`);
} }

View File

@@ -48,6 +48,15 @@ export const collection = {
]), ]),
vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy), vectorEmbeddingPolicy: ko.observable<DataModels.VectorEmbeddingPolicy>({} as DataModels.VectorEmbeddingPolicy),
fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy), fullTextPolicy: ko.observable<DataModels.FullTextPolicy>({} as DataModels.FullTextPolicy),
materializedViews: ko.observable<DataModels.MaterializedView[]>([
{ id: "view1", _rid: "rid1" },
{ id: "view2", _rid: "rid2" },
]),
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
definition: "SELECT * FROM c WHERE c.id = 1",
sourceCollectionId: "source1",
sourceCollectionRid: "rid123",
}),
readSettings: () => { readSettings: () => {
return; return;
}, },

View File

@@ -60,6 +60,8 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
"indexingPolicy": [Function], "indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function], "offer": [Function],
"partitionKey": { "partitionKey": {
"kind": "hash", "kind": "hash",
@@ -139,6 +141,8 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
"indexingPolicy": [Function], "indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function], "offer": [Function],
"partitionKey": { "partitionKey": {
"kind": "hash", "kind": "hash",
@@ -258,6 +262,8 @@ exports[`SettingsComponent renders 1`] = `
"getDatabase": [Function], "getDatabase": [Function],
"id": [Function], "id": [Function],
"indexingPolicy": [Function], "indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function], "offer": [Function],
"partitionKey": { "partitionKey": {
"kind": "hash", "kind": "hash",
@@ -336,6 +342,101 @@ exports[`SettingsComponent renders 1`] = `
shouldDiscardComputedProperties={false} shouldDiscardComputedProperties={false}
/> />
</PivotItem> </PivotItem>
<PivotItem
headerText="Materialized Views (Preview)"
itemKey="MaterializedViewTab"
key="MaterializedViewTab"
style={
{
"marginTop": 20,
}
}
>
<MaterializedViewComponent
collection={
{
"analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],
"fullTextPolicy": [Function],
"geospatialConfig": [Function],
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"materializedViewDefinition": [Function],
"materializedViews": [Function],
"offer": [Function],
"partitionKey": {
"kind": "hash",
"paths": [],
"version": 2,
},
"partitionKeyProperties": [
"partitionKey",
],
"readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function],
}
}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
/>
</PivotItem>
</StyledPivot> </StyledPivot>
</div> </div>
</div> </div>

View File

@@ -35,12 +35,20 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setIsThroughputCapExceeded, setIsThroughputCapExceeded,
onCostAcknowledgeChange, onCostAcknowledgeChange,
}: ThroughputInputProps) => { }: ThroughputInputProps) => {
const defaultThroughput: number = let defaultThroughput: number;
const workloadType: Constants.WorkloadType = getWorkloadType();
if (
isFreeTier || isFreeTier ||
isQuickstart || isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType()) [Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
? AutoPilotUtils.autoPilotThroughput1K ) {
: AutoPilotUtils.autoPilotThroughput4K; defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
} else if (workloadType === Constants.WorkloadType.Production) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
} else {
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
}
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true); const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(defaultThroughput); const [throughput, setThroughput] = useState<number>(defaultThroughput);

View File

@@ -8,7 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient"; import { IGalleryItem } from "Juno/JunoClient";
import { scheduleRefreshDatabaseResourceToken } from "Platform/Fabric/FabricUtil"; import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
@@ -43,7 +43,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs"; import { ReactTabKind, useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog"; import { DialogProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
@@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager";
import { NotebookPaneContent } from "./Notebook/NotebookManager"; import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook"; import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
@@ -187,6 +187,10 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
} }
if (isFabricMirrored()) {
useTabs.getState().closeReactTab(ReactTabKind.Home);
}
this.refreshExplorer(); this.refreshExplorer();
} }
@@ -347,8 +351,8 @@ export default class Explorer {
}; };
public onRefreshResourcesClick = async (): Promise<void> => { public onRefreshResourcesClick = async (): Promise<void> => {
if (configContext.platform === Platform.Fabric) { if (isFabricMirroredKey()) {
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases()); scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
return; return;
} }
@@ -1127,7 +1131,7 @@ export default class Explorer {
await this.initNotebooks(userContext.databaseAccount); await this.initNotebooks(userContext.databaseAccount);
} }
await this.refreshSampleData(); this.refreshSampleData();
} }
public async configureCopilot(): Promise<void> { public async configureCopilot(): Promise<void> {
@@ -1152,26 +1156,27 @@ export default class Explorer {
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled); .setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
} }
public async refreshSampleData(): Promise<void> { public refreshSampleData(): void {
try { if (!userContext.sampleDataConnectionInfo) {
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");
return; 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");
});
} }
} }

View File

@@ -14,10 +14,6 @@
.flex-direction(@direction: row); .flex-direction(@direction: row);
padding: 4px 5px; padding: 4px 5px;
label {
padding: 0px;
}
.valueCol { .valueCol {
flex-grow: 1; flex-grow: 1;
padding-right: 5px; padding-right: 5px;
@@ -63,6 +59,10 @@
height: 100%; height: 100%;
} }
.customTrashIcon {
padding-top: 33px;
}
.rightPaneTrashIconImg { .rightPaneTrashIconImg {
vertical-align: top; vertical-align: top;
} }

View File

@@ -142,10 +142,11 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
<div className="labelCol"> <div className="labelCol">
<TextField <TextField
className="edgeInput" className="edgeInput"
label={index === 0 && "Key"}
type="text" type="text"
id="propertyKeyNewVertexPane" id="propertyKeyNewVertexPane"
componentRef={input} componentRef={input}
aria-required="true" required
placeholder="Key" placeholder="Key"
autoComplete="off" autoComplete="off"
aria-label={`Enter value for propery ${index + 1}`} 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)} onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
/> />
</div> </div>
<span className="mandatoryStar">*&nbsp;</span>
<div className="valueCol"> <div className="valueCol">
<TextField <TextField
className="edgeInput" className="edgeInput"
label={index === 0 && "Value"}
type="text" type="text"
placeholder="Value" placeholder="Value"
autoComplete="off" autoComplete="off"
@@ -169,6 +170,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
<div> <div>
<Dropdown <Dropdown
role="combobox" role="combobox"
label={index === 0 && "Type"}
ariaLabel="Type"
placeholder="Select an option" placeholder="Select an option"
defaultSelectedKey={data.values[0].type} defaultSelectedKey={data.values[0].type}
style={{ width: 100 }} style={{ width: 100 }}
@@ -181,7 +184,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
</div> </div>
<div className="actionCol"> <div className="actionCol">
<div <div
className="rightPaneTrashIcon rightPaneBtns" className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
tabIndex={0} tabIndex={0}
role="button" role="button"
aria-label={`Delete ${data.key}`} aria-label={`Delete ${data.key}`}

View File

@@ -6,12 +6,12 @@
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants"; import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants"; import { StyleConstants } from "../../../Common/StyleConstants";
import { Platform, configContext } from "../../../ConfigContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@@ -93,19 +93,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
); );
} }
const rootStyle = const rootStyle = isFabric()
configContext.platform === Platform.Fabric ? {
? { root: {
root: { backgroundColor: "transparent",
backgroundColor: "transparent", padding: "2px 8px 0px 8px",
padding: "2px 8px 0px 8px", },
}, }
} : {
: { root: {
root: { backgroundColor: backgroundColor,
backgroundColor: backgroundColor, },
}, };
};
const allButtons = staticButtons.concat(contextButtons).concat(controlButtons); const allButtons = staticButtons.concat(contextButtons).concat(controlButtons);
const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons); const keyboardHandlers = CommandBarUtil.createKeyboardHandlers(allButtons);

View File

@@ -37,21 +37,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(enableAzureSynapseLinkBtn).toBeDefined(); expect(enableAzureSynapseLinkBtn).toBeDefined();
}); });
it("Button should not be visible for Tables API", () => { // TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
updateUserContext({ // Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
databaseAccount: { // unsupported in jest and needs to be tested with react-hooks-testing-library.
properties: { //
capabilities: [{ name: "EnableTable" }], // it("Button should not be visible for Tables API", () => {
}, // updateUserContext({
} as DatabaseAccount, // databaseAccount: {
}); // properties: {
// capabilities: [{ name: "EnableTable" }],
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); // },
const enableAzureSynapseLinkBtn = buttons.find( // } as DatabaseAccount,
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel, // });
); //
expect(enableAzureSynapseLinkBtn).toBeUndefined(); // 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", () => { it("Button should not be visible for Cassandra API", () => {
updateUserContext({ updateUserContext({

View File

@@ -1,4 +1,5 @@
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardAction } from "KeyboardShortcuts";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import * as React from "react"; import * as React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg"; 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 [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);

View File

@@ -37,6 +37,10 @@
background-color:@NotificationHigh; background-color:@NotificationHigh;
} }
&:focus {
.focusedBorder();
}
.statusBar { .statusBar {
.dataTypeIcons { .dataTypeIcons {
cursor: pointer; cursor: pointer;

View File

@@ -81,10 +81,6 @@ export class NotificationConsoleComponent extends React.Component<
} }
} }
public setElememntRef = (element: HTMLElement): void => {
this.consoleHeaderElement = element;
};
public render(): JSX.Element { public render(): JSX.Element {
const numInProgress = this.state.allConsoleData.filter( const numInProgress = this.state.allConsoleData.filter(
(data: ConsoleData) => data.type === ConsoleDataType.InProgress, (data: ConsoleData) => data.type === ConsoleDataType.InProgress,
@@ -101,7 +97,9 @@ export class NotificationConsoleComponent extends React.Component<
<div <div
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
ref={this.setElememntRef} role="button"
aria-label="Console"
aria-expanded={this.props.isConsoleExpanded}
onClick={() => this.expandCollapseConsole()} onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)} onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0} tabIndex={0}
@@ -109,15 +107,15 @@ export class NotificationConsoleComponent extends React.Component<
<div className="statusBar"> <div className="statusBar">
<span className="dataTypeIcons"> <span className="dataTypeIcons">
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={LoadingIcon} alt="in progress items" /> <img src={LoadingIcon} alt="In progress items" />
<span className="numInProgress">{numInProgress}</span> <span className="numInProgress">{numInProgress}</span>
</span> </span>
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={ErrorBlackIcon} alt="error items" /> <img src={ErrorBlackIcon} alt="Error items" />
<span className="numErroredItems">{numErroredItems}</span> <span className="numErroredItems">{numErroredItems}</span>
</span> </span>
<span className="notificationConsoleHeaderIconWithData"> <span className="notificationConsoleHeaderIconWithData">
<img src={infoBubbleIcon} alt="info items" /> <img src={infoBubbleIcon} alt="Info items" />
<span className="numInfoItems">{numInfoItems}</span> <span className="numInfoItems">{numInfoItems}</span>
</span> </span>
</span> </span>
@@ -129,17 +127,10 @@ export class NotificationConsoleComponent extends React.Component<
</span> </span>
</span> </span>
</div> </div>
<div <div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
aria-expanded={!this.props.isConsoleExpanded}
>
<img <img
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon} src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"} alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
/> />
</div> </div>
</div> </div>
@@ -259,9 +250,6 @@ export class NotificationConsoleComponent extends React.Component<
} }
private onConsoleWasExpanded = (): void => { private onConsoleWasExpanded = (): void => {
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
this.consoleHeaderElement.focus();
}
useNotificationConsole.getState().setConsoleAnimationFinished(true); useNotificationConsole.getState().setConsoleAnimationFinished(true);
}; };

View File

@@ -5,10 +5,13 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleContainer" className="notificationConsoleContainer"
> >
<div <div
aria-expanded={false}
aria-label="Console"
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<div <div
@@ -21,7 +24,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="in progress items" alt="In progress items"
src={{}} src={{}}
/> />
<span <span
@@ -34,7 +37,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="error items" alt="Error items"
src={{}} src={{}}
/> />
<span <span
@@ -47,7 +50,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="info items" alt="Info items"
src={{}} src={{}}
/> />
<span <span
@@ -71,15 +74,11 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
</span> </span>
</div> </div>
<div <div
aria-expanded={true}
aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
> >
<img <img
alt="ChevronUpIcon" alt="Expand icon"
src="" src=""
/> />
</div> </div>
@@ -176,10 +175,13 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleContainer" className="notificationConsoleContainer"
> >
<div <div
aria-expanded={false}
aria-label="Console"
className="notificationConsoleHeader" className="notificationConsoleHeader"
id="notificationConsoleHeader" id="notificationConsoleHeader"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={0} tabIndex={0}
> >
<div <div
@@ -192,7 +194,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="in progress items" alt="In progress items"
src={{}} src={{}}
/> />
<span <span
@@ -205,7 +207,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="error items" alt="Error items"
src={{}} src={{}}
/> />
<span <span
@@ -218,7 +220,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleHeaderIconWithData" className="notificationConsoleHeaderIconWithData"
> >
<img <img
alt="info items" alt="Info items"
src={{}} src={{}}
/> />
<span <span
@@ -244,15 +246,11 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
</span> </span>
</div> </div>
<div <div
aria-expanded={true}
aria-label="console button collapsed"
className="expandCollapseButton" className="expandCollapseButton"
data-test="NotificationConsole/ExpandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton"
role="button"
tabIndex={0}
> >
<img <img
alt="ChevronUpIcon" alt="Expand icon"
src="" src=""
/> />
</div> </div>

View File

@@ -1,6 +1,5 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { PhoenixClient } from "Phoenix/PhoenixClient"; import { PhoenixClient } from "Phoenix/PhoenixClient";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -128,9 +127,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
? databaseAccount?.location ? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase(); : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.DisallowedLocations) const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
? `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`
: `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
try { try {
const response = await fetch(disallowedLocationsUri, { const response = await fetch(disallowedLocationsUri, {

View File

@@ -1,6 +1,6 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled. // 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 { useDatabases } from "Explorer/useDatabases";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts"; import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -58,9 +58,9 @@ function openCollectionTab(
} }
if ( 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.SQLDocuments ||
action.tabKind === ActionContracts.TabKind.SQLQuery action.tabKind === ActionContracts.TabKind.SQLQuery

View File

@@ -1,6 +1,6 @@
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import Explorer from "../Explorer"; import Explorer from "../../Explorer";
import { AddCollectionPanel } from "./AddCollectionPanel"; import { AddCollectionPanel } from "./AddCollectionPanel";
const props = { const props = {

View File

@@ -21,13 +21,28 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
FullTextPoliciesComponent,
getFullTextLanguageOptions,
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
AllPropertiesIndexed,
AnalyticalStorageContent,
ContainerVectorPolicyTooltipContent,
FullTextPolicyDefault,
getPartitionKey,
getPartitionKeyName,
getPartitionKeyPlaceHolder,
getPartitionKeyTooltipText,
isFreeTierAccount,
isSynapseLinkEnabled,
parseUniqueKeys,
scrollToSection,
SharedDatabaseDefault,
shouldShowAnalyticalStoreOptions,
UniqueKeysHeader,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { CollectionCreation } from "Shared/Constants"; import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -41,15 +56,15 @@ import {
isVectorSearchEnabled, isVectorSearchEnabled,
} from "Utils/CapabilityUtils"; } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils"; import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import "../Controls/ThroughputInput/ThroughputInput.less"; import "../../Controls/ThroughputInput/ThroughputInput.less";
import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator"; import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
import Explorer from "../Explorer"; import Explorer from "../../Explorer";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../../useDatabases";
import { PanelFooterComponent } from "./PanelFooterComponent"; import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen"; import { PanelLoadingScreen } from "../PanelLoadingScreen";
export interface AddCollectionPanelProps { export interface AddCollectionPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -57,40 +72,6 @@ export interface AddCollectionPanelProps {
isQuickstart?: boolean; isQuickstart?: boolean;
} }
const SharedDatabaseDefault: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [],
excludedPaths: [
{
path: "/*",
},
],
};
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
indexes: [
{
kind: "Range",
dataType: "Number",
precision: -1,
},
{
kind: "Range",
dataType: "String",
precision: -1,
},
],
},
],
excludedPaths: [],
};
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
vectorEmbeddings: [], vectorEmbeddings: [],
}; };
@@ -143,7 +124,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "", collectionId: props.isQuickstart ? `Sample${getCollectionName()}` : "",
enableIndexing: true, enableIndexing: true,
isSharded: userContext.apiType !== "Tables", isSharded: userContext.apiType !== "Tables",
partitionKey: this.getPartitionKey(), partitionKey: getPartitionKey(props.isQuickstart),
subPartitionKeys: [], subPartitionKeys: [],
enableDedicatedThroughput: false, enableDedicatedThroughput: false,
createMongoWildCardIndex: createMongoWildCardIndex:
@@ -159,7 +140,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
vectorEmbeddingPolicy: [], vectorEmbeddingPolicy: [],
vectorIndexingPolicy: [], vectorIndexingPolicy: [],
vectorPolicyValidated: true, vectorPolicyValidated: true,
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] }, fullTextPolicy: FullTextPolicyDefault,
fullTextIndexes: [], fullTextIndexes: [],
fullTextPolicyValidated: true, fullTextPolicyValidated: true,
}; };
@@ -173,7 +154,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void { componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) { if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
this.scrollToSection("panelContainer"); scrollToSection("panelContainer");
} }
} }
@@ -190,7 +171,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{!this.state.errorMessage && this.isFreeTierAccount() && ( {!this.state.errorMessage && isFreeTierAccount() && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)} message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
messageType="info" messageType="info"
@@ -284,150 +265,152 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
<div className="panelMainContent"> <div className="panelMainContent">
<Stack hidden={userContext.apiType === "Tables"}> {!(isFabricNative() && this.props.databaseId !== undefined) && (
<Stack horizontal> <Stack hidden={userContext.apiType === "Tables"}>
<span className="mandatoryStar">*&nbsp;</span> <Stack horizontal>
<Text className="panelTextBold" variant="small"> <span className="mandatoryStar">*&nbsp;</span>
Database {userContext.apiType === "Mongo" ? "name" : "id"} <Text className="panelTextBold" variant="small">
</Text> Database {userContext.apiType === "Mongo" ? "name" : "id"}
<TooltipHost </Text>
directionalHint={DirectionalHint.bottomLeftEdge} <TooltipHost
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName( directionalHint={DirectionalHint.bottomLeftEdge}
true, content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
).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(
true, true,
).toLocaleLowerCase()}.`} ).toLocaleLowerCase()}.`}
/> >
</TooltipHost> <Icon
</Stack> iconName="Info"
className="panelInfoIcon"
{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} 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> </TooltipHost>
<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> </Stack>
)}
{this.state.createNewDatabase && ( {configContext.platform !== Platform.Fabric && (
<Stack className="panelGroupSpacing"> <Stack horizontal verticalAlign="center">
<input <div role="radiogroup">
name="newDatabaseId" <input
id="newDatabaseId" className="panelRadioBtn"
aria-required checked={this.state.createNewDatabase}
required aria-label="Create new database"
type="text" aria-checked={this.state.createNewDatabase}
autoComplete="off" name="databaseType"
pattern="[^/?#\\]*[^/?# \\]" type="radio"
title="May not end with space nor contain characters '\' '/' '#' '?'" role="radio"
placeholder="Type a new database id" id="databaseCreateNew"
size={40} tabIndex={0}
className="panelTextField" onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
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 <span className="panelRadioBtnLabel">Create new</span>
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName( <input
true, className="panelRadioBtn"
).toLocaleLowerCase()} within the database.`} checked={!this.state.createNewDatabase}
> aria-label="Use existing database"
<Icon aria-checked={!this.state.createNewDatabase}
iconName="Info" name="databaseType"
className="panelInfoIcon" type="radio"
tabIndex={0} role="radio"
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName( 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="[^/?#\\]*[^/?# \\]"
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 })
}
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true, true,
).toLocaleLowerCase()} within the database.`} ).toLocaleLowerCase()} within the database.`}
/> >
</TooltipHost> <Icon
</Stack> 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 && ( {!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true} isDatabase={true}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()} isFreeTier={isFreeTierAccount()}
isQuickstart={this.props.isQuickstart} isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)} setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded }) this.setState({ isThroughputCapExceeded })
} }
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)} onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/> />
)} )}
</Stack> </Stack>
)} )}
{!this.state.createNewDatabase && ( {!this.state.createNewDatabase && (
<Dropdown <Dropdown
ariaLabel="Choose an existing database" ariaLabel="Choose an existing database"
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }} styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }} style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database" placeholder="Choose an existing database"
options={this.getDatabaseOptions()} options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) => onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string }) this.setState({ selectedDatabaseId: database.key as string })
} }
defaultSelectedKey={this.props.databaseId} defaultSelectedKey={this.props.databaseId}
responsiveMode={999} responsiveMode={999}
/> />
)} )}
<Separator className="panelSeparator" /> <Separator className="panelSeparator" />
</Stack> </Stack>
)}
<Stack> <Stack>
<Stack horizontal> <Stack horizontal>
@@ -576,17 +559,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
{this.getPartitionKeyName()} {getPartitionKeyName()}
</Text> </Text>
<TooltipHost <TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
directionalHint={DirectionalHint.bottomLeftEdge}
content={this.getPartitionKeyTooltipText()}
>
<Icon <Icon
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={this.getPartitionKeyTooltipText()} ariaLabel={getPartitionKeyTooltipText()}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -600,8 +580,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
required required
size={40} size={40}
className="panelTextField" className="panelTextField"
placeholder={this.getPartitionKeyPlaceHolder()} placeholder={getPartitionKeyPlaceHolder()}
aria-label={this.getPartitionKeyName()} aria-label={getPartitionKeyName()}
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"} pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""} title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
value={this.state.partitionKey} value={this.state.partitionKey}
@@ -639,8 +619,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={index > 0 ? 1 : 0} tabIndex={index > 0 ? 1 : 0}
className="panelTextField" className="panelTextField"
autoComplete="off" autoComplete="off"
placeholder={this.getPartitionKeyPlaceHolder(index)} placeholder={getPartitionKeyPlaceHolder(index)}
aria-label={this.getPartitionKeyName()} aria-label={getPartitionKeyName()}
pattern={".*"} pattern={".*"}
title={""} title={""}
value={subPartitionKey} value={subPartitionKey}
@@ -666,7 +646,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
); );
})} })}
{userContext.apiType === "SQL" && ( {!isFabricNative() && userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<DefaultButton <DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }} styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
@@ -731,10 +711,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowCollectionThroughputInput() && ( {this.shouldShowCollectionThroughputInput() && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false} isDatabase={false}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()} isFreeTier={isFreeTierAccount()}
isQuickstart={this.props.isQuickstart} isQuickstart={this.props.isQuickstart}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@@ -747,29 +727,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
)} )}
{userContext.apiType === "SQL" && ( {!isFabricNative() && userContext.apiType === "SQL" && (
<Stack> <Stack>
<Stack horizontal> {UniqueKeysHeader()}
<Text className="panelTextBold" variant="small">
Unique keys
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
}
/>
</TooltipHost>
</Stack>
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
return ( return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal> <Stack style={{ marginBottom: 8 }} key={`uniqueKey${i}`} horizontal>
@@ -817,10 +777,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{this.shouldShowAnalyticalStoreOptions() && ( {shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
{this.getAnalyticalStorageContent()} {AnalyticalStorageContent()}
</Text> </Text>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
@@ -828,7 +788,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.enableAnalyticalStore} checked={this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store" aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore} aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -843,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore} checked={!this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store" aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore} aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
@@ -857,7 +817,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div> </div>
</Stack> </Stack>
{!this.isSynapseLinkEnabled() && ( {!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text variant="small"> <Text variant="small">
Azure Synapse Link is required for creating an analytical store{" "} Azure Synapse Link is required for creating an analytical store{" "}
@@ -887,9 +847,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Vector Policy" title="Container Vector Policy"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
this.scrollToSection("collapsibleVectorPolicySectionContent"); scrollToSection("collapsibleVectorPolicySectionContent");
}} }}
tooltipContent={this.getContainerVectorPolicyTooltipContent()} tooltipContent={ContainerVectorPolicyTooltipContent()}
> >
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}> <Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}> <Stack styles={{ root: { paddingLeft: 40 } }}>
@@ -915,7 +875,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
title="Container Full Text Search Policy" title="Container Full Text Search Policy"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
this.scrollToSection("collapsibleFullTextPolicySectionContent"); scrollToSection("collapsibleFullTextPolicySectionContent");
}} }}
//TODO: uncomment when learn more text becomes available //TODO: uncomment when learn more text becomes available
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()} // tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
@@ -937,13 +897,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
</Stack> </Stack>
)} )}
{userContext.apiType !== "Tables" && ( {!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title="Advanced"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToSection("collapsibleAdvancedSectionContent"); scrollToSection("collapsibleAdvancedSectionContent");
}} }}
> >
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent"> <Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
@@ -1053,31 +1013,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
})); }));
} }
private getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
private getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return "e.g., categoryId";
case "Gremlin":
return "e.g., /address";
case "SQL":
return `${
index === undefined
? "Required - first partition key e.g., /TenantId"
: index === 0
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return "e.g., /address/zipCode";
}
}
private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void { private onCreateNewDatabaseRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && !this.state.createNewDatabase) { if (event.target.checked && !this.state.createNewDatabase) {
this.setState({ this.setState({
@@ -1165,48 +1100,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return !!selectedDatabase?.offer(); return !!selectedDatabase?.offer();
} }
private isFreeTierAccount(): boolean {
return userContext.databaseAccount?.properties?.enableFreeTier;
}
private getFreeTierIndexingText(): string { private getFreeTierIndexingText(): string {
return this.state.enableIndexing return this.state.enableIndexing
? "All properties in your documents will be indexed by default for flexible and efficient queries." ? "All properties in your documents will be indexed by default for flexible and efficient queries."
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations."; : "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
} }
private getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${this.getPartitionKeyName(
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (userContext.apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
private getPartitionKey(): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
if (this.props.isQuickstart) {
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
}
return "";
}
private getPartitionKeySubtext(): string { private getPartitionKeySubtext(): string {
if ( if (
userContext.features.partitionKeyDefault && userContext.features.partitionKeyDefault &&
@@ -1218,34 +1117,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return ""; return "";
} }
private getAnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without
impacting the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
Learn more
</Link>
</Text>
);
}
private getContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more
</Link>
</Text>
);
}
//TODO: uncomment when learn more text becomes available //TODO: uncomment when learn more text becomes available
// private getContainerFullTextPolicyTooltipContent(): JSX.Element { // private getContainerFullTextPolicyTooltipContent(): JSX.Element {
// return ( // return (
@@ -1260,7 +1131,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// } // }
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) { if (isFabricNative() || isServerlessAccount()) {
return false; return false;
} }
@@ -1276,7 +1147,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowIndexingOptionsForFreeTierAccount(): boolean { private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
if (!this.isFreeTierAccount()) { if (!isFreeTierAccount()) {
return false; return false;
} }
@@ -1285,39 +1156,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: this.isSelectedDatabaseSharedThroughput(); : this.isSelectedDatabaseSharedThroughput();
} }
private shouldShowAnalyticalStoreOptions(): boolean {
if (configContext.platform === Platform.Emulator) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
default:
return false;
}
}
private isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
if (properties.enableAnalyticalStorage) {
return true;
}
return properties.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
private shouldShowVectorSearchParameters() { private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput()); return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
} }
@@ -1398,11 +1236,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private getAnalyticalStorageTtl(): number { private getAnalyticalStorageTtl(): number {
if (!this.isSynapseLinkEnabled()) { if (!isSynapseLinkEnabled()) {
return undefined; return undefined;
} }
if (!this.shouldShowAnalyticalStoreOptions()) { if (!shouldShowAnalyticalStoreOptions()) {
return undefined; return undefined;
} }
@@ -1416,10 +1254,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled; return Constants.AnalyticalStorageTtl.Disabled;
} }
private scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
private getSampleDBName(): string { private getSampleDBName(): string {
const existingSampleDBs = useDatabases const existingSampleDBs = useDatabases
.getState() .getState()
@@ -1454,7 +1288,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKeyString = "/'$pk'"; partitionKeyString = "/'$pk'";
} }
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys(); const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2; const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
const partitionKey: DataModels.PartitionKey = partitionKeyString const partitionKey: DataModels.PartitionKey = partitionKeyString
? { ? {

View File

@@ -0,0 +1,217 @@
import { DirectionalHint, Icon, Link, Stack, Text, TooltipHost } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { userContext } from "UserContext";
export function getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${getPartitionKeyName(
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (userContext.apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
export function getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
export function getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return "e.g., categoryId";
case "Gremlin":
return "e.g., /address";
case "SQL":
return `${
index === undefined
? "Required - first partition key e.g., /TenantId"
: index === 0
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return "e.g., /address/zipCode";
}
}
export function getPartitionKey(isQuickstart?: boolean): string {
if (userContext.apiType !== "SQL" && userContext.apiType !== "Mongo") {
return "";
}
if (userContext.features.partitionKeyDefault) {
return userContext.apiType === "SQL" ? "/id" : "_id";
}
if (userContext.features.partitionKeyDefault2) {
return userContext.apiType === "SQL" ? "/pk" : "pk";
}
if (isQuickstart) {
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
}
return "";
}
export function isFreeTierAccount(): boolean {
return userContext.databaseAccount?.properties?.enableFreeTier;
}
export function UniqueKeysHeader(): JSX.Element {
const tooltipContent =
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
return (
<Stack horizontal>
<Text className="panelTextBold" variant="small">
Unique keys
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
</TooltipHost>
</Stack>
);
}
export function shouldShowAnalyticalStoreOptions(): boolean {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
default:
return false;
}
}
export function AnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
Learn more
</Link>
</Text>
);
}
export function isSynapseLinkEnabled(): boolean {
if (!userContext.databaseAccount) {
return false;
}
const { properties } = userContext.databaseAccount;
if (!properties) {
return false;
}
if (properties.enableAnalyticalStorage) {
return true;
}
return properties.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics,
);
}
export function scrollToSection(id: string): void {
document.getElementById(id)?.scrollIntoView();
}
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more
</Link>
</Text>
);
}
export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy {
if (uniqueKeys?.length === 0) {
return undefined;
}
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] };
uniqueKeys.forEach((uniqueKey: string) => {
if (uniqueKey) {
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
if (trimmedPaths?.length > 0) {
if (userContext.apiType === "Mongo") {
trimmedPaths.map((path) => {
const transformedPath = path.split(".").join("/");
if (transformedPath[0] !== "/") {
return "/" + transformedPath;
}
return transformedPath;
});
}
uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths });
}
}
});
return uniqueKeyPolicy;
}
export const SharedDatabaseDefault: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [],
excludedPaths: [
{
path: "/*",
},
],
};
export const FullTextPolicyDefault: DataModels.FullTextPolicy = {
defaultLanguage: getFullTextLanguageOptions()[0].key as never,
fullTextPaths: [],
};
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
indexes: [
{
kind: "Range",
dataType: "Number",
precision: -1,
},
{
kind: "Range",
dataType: "String",
precision: -1,
},
],
},
],
excludedPaths: [],
};

View File

@@ -0,0 +1,54 @@
import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
export interface AddMVAdvancedComponentProps {
useHashV1: boolean;
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const AddMVAdvancedComponent = (props: AddMVAdvancedComponentProps): JSX.Element => {
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
setUseHashV1(isChecked);
setSubPartitionKeys([]);
};
return (
<CollapsibleSectionComponent
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddMaterializedViewPaneAdvancedSection);
scrollToSection("collapsibleAdvancedSectionContent");
}}
>
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
<Checkbox
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
checked={useHashV1}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
useHashV1CheckboxOnChange(isChecked);
}}
/>
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the created
container will use a legacy partitioning scheme that supports partition key values of size only up to 101
bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more
</Link>
</Text>
</Stack>
</CollapsibleSectionComponent>
);
};

View File

@@ -0,0 +1,99 @@
import { DefaultButton, Link, Stack, Text } from "@fluentui/react";
import * as Constants from "Common/Constants";
import Explorer from "Explorer/Explorer";
import {
AnalyticalStorageContent,
isSynapseLinkEnabled,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { getCollectionName } from "Utils/APITypeUtils";
export interface AddMVAnalyticalStoreComponentProps {
explorer: Explorer;
enableAnalyticalStore: boolean;
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
}
export const AddMVAnalyticalStoreComponent = (props: AddMVAnalyticalStoreComponentProps): JSX.Element => {
const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props;
const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => {
if (checked && !enableAnalyticalStore) {
setEnableAnalyticalStore(true);
}
};
const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => {
if (checked && enableAnalyticalStore) {
setEnableAnalyticalStore(false);
}
};
return (
<Stack className="panelGroupSpacing">
<Text className="panelTextBold" variant="small">
{AnalyticalStorageContent()}
</Text>
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-checked={enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-checked={!enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
}}
/>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small">
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
Enable Synapse Link for this Cosmos DB account.{" "}
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
Learn more
</Link>
</Text>
<DefaultButton
text="Enable"
onClick={() => explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
/>
</Stack>
)}
</Stack>
);
};

View File

@@ -0,0 +1,45 @@
import { Stack } from "@fluentui/react";
import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface AddMVFullTextSearchComponentProps {
fullTextPolicy: FullTextPolicy;
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
}
export const AddMVFullTextSearchComponent = (props: AddMVFullTextSearchComponentProps): JSX.Element => {
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
return (
<Stack>
<CollapsibleSectionComponent
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
}}
>
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<FullTextPoliciesComponent
fullTextPolicy={fullTextPolicy}
onFullTextPathChange={(
fullTextPolicy: FullTextPolicy,
fullTextIndexes: FullTextIndex[],
fullTextPolicyValidated: boolean,
) => {
setFullTextPolicy(fullTextPolicy);
setFullTextIndexes(fullTextIndexes);
setFullTextPolicyValidated(fullTextPolicyValidated);
}}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
);
};

View File

@@ -0,0 +1,132 @@
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
import * as Constants from "Common/Constants";
import {
getPartitionKeyName,
getPartitionKeyPlaceHolder,
getPartitionKeyTooltipText,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface AddMVPartitionKeyComponentProps {
partitionKey?: string;
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
subPartitionKeys: string[];
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
useHashV1: boolean;
}
export const AddMVPartitionKeyComponent = (props: AddMVPartitionKeyComponentProps): JSX.Element => {
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
const partitionKeyValueOnChange = (value: string): void => {
if (!partitionKey && !value.startsWith("/")) {
setPartitionKey("/" + value);
} else {
setPartitionKey(value);
}
};
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
updatedSubPartitionKeys[index] = "/" + value.trim();
} else {
updatedSubPartitionKeys[index] = value.trim();
}
setSubPartitionKeys(updatedSubPartitionKeys);
};
return (
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Partition key
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</Stack>
<input
type="text"
id="addmaterializedView-partitionKeyValue"
aria-required
required
size={40}
className="panelTextField"
placeholder={getPartitionKeyPlaceHolder()}
aria-label={getPartitionKeyName()}
pattern=".*"
value={partitionKey}
style={{ marginBottom: 8 }}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
partitionKeyValueOnChange(event.target.value);
}}
/>
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
<div
style={{
width: "20px",
border: "solid",
borderWidth: "0px 0px 1px 1px",
marginRight: "5px",
}}
></div>
<input
type="text"
id="addMaterializedView-partitionKeyValue"
key={`addMaterializedView-partitionKeyValue_${subPartitionKeyIndex}`}
aria-required
required
size={40}
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
className="panelTextField"
autoComplete="off"
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
aria-label={getPartitionKeyName()}
pattern={".*"}
title={""}
value={subPartitionKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
const updatedSubPartitionKeys = subPartitionKeys.filter(
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
);
setSubPartitionKeys(updatedSubPartitionKeys);
}}
/>
</Stack>
);
})}
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
hidden={useHashV1}
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
>
Add hierarchical partition key
</DefaultButton>
{subPartitionKeys.length > 0 && (
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to partition your
data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview
JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
Learn more
</Link>
</Text>
)}
</Stack>
</Stack>
);
};

View File

@@ -0,0 +1,71 @@
import { Checkbox, Stack } from "@fluentui/react";
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useDatabases } from "Explorer/useDatabases";
import React from "react";
import { getCollectionName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils";
export interface AddMVThroughputComponentProps {
enableDedicatedThroughput: boolean;
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
isSelectedSourceContainerSharedThroughput: () => boolean;
showCollectionThroughputInput: () => boolean;
materializedViewThroughputOnChange: (materializedViewThroughputValue: number) => void;
isMaterializedViewAutoscaleOnChange: (isMaterializedViewAutoscaleValue: boolean) => void;
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
}
export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): JSX.Element => {
const {
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
materializedViewThroughputOnChange,
isMaterializedViewAutoscaleOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
} = props;
return (
<Stack>
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
checked={enableDedicatedThroughput}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
/>
</Stack>
)}
{showCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
isFreeTier={isFreeTierAccount()}
isQuickstart={false}
setThroughputValue={(throughput: number) => {
materializedViewThroughputOnChange(throughput);
}}
setIsAutoscale={(isAutoscale: boolean) => {
isMaterializedViewAutoscaleOnChange(isAutoscale);
}}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
setIsThroughputCapExceeded(isThroughputCapExceeded);
}}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
isCostAknowledgedOnChange(isAcknowledged);
}}
/>
)}
</Stack>
);
};

View File

@@ -0,0 +1,78 @@
import { ActionButton, IconButton, Stack } from "@fluentui/react";
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
import { userContext } from "UserContext";
export interface AddMVUniqueKeysComponentProps {
uniqueKeys: string[];
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
}
export const AddMVUniqueKeysComponent = (props: AddMVUniqueKeysComponentProps): JSX.Element => {
const { uniqueKeys, setUniqueKeys } = props;
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
return value;
}
return uniqueKey;
});
setUniqueKeys(updatedUniqueKeys);
};
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
setUniqueKeys(updatedUniqueKeys);
};
const addUniqueKeyOnClick = (): void => {
setUniqueKeys([...uniqueKeys, ""]);
};
return (
<Stack>
{UniqueKeysHeader()}
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
<input
type="text"
autoComplete="off"
placeholder={
userContext.apiType === "Mongo"
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
autoFocus
value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
deleteUniqueKeyOnClick(uniqueKeyIndex);
}}
/>
</Stack>
);
})}
<ActionButton
iconProps={{ iconName: "Add" }}
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
onClick={() => {
addUniqueKeyOnClick();
}}
>
Add unique key
</ActionButton>
</Stack>
);
};

View File

@@ -0,0 +1,58 @@
import { Stack } from "@fluentui/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import {
ContainerVectorPolicyTooltipContent,
scrollToSection,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import React from "react";
export interface AddMVVectorSearchComponentProps {
vectorEmbeddingPolicy: VectorEmbedding[];
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
vectorIndexingPolicy: VectorIndex[];
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
}
export const AddMVVectorSearchComponent = (props: AddMVVectorSearchComponentProps): JSX.Element => {
const {
vectorEmbeddingPolicy,
setVectorEmbeddingPolicy,
vectorIndexingPolicy,
setVectorIndexingPolicy,
setVectorPolicyValidated,
} = props;
return (
<Stack>
<CollapsibleSectionComponent
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
}}
tooltipContent={ContainerVectorPolicyTooltipContent()}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
<VectorEmbeddingPoliciesComponent
vectorEmbeddings={vectorEmbeddingPolicy}
vectorIndexes={vectorIndexingPolicy}
onVectorEmbeddingChange={(
vectorEmbeddingPolicy: VectorEmbedding[],
vectorIndexingPolicy: VectorIndex[],
vectorPolicyValidated: boolean,
) => {
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
setVectorIndexingPolicy(vectorIndexingPolicy);
setVectorPolicyValidated(vectorPolicyValidated);
}}
/>
</Stack>
</Stack>
</CollapsibleSectionComponent>
</Stack>
);
};

View File

@@ -0,0 +1,28 @@
import { shallow, ShallowWrapper } from "enzyme";
import Explorer from "Explorer/Explorer";
import {
AddMaterializedViewPanel,
AddMaterializedViewPanelProps,
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
import React, { Component } from "react";
const props: AddMaterializedViewPanelProps = {
explorer: new Explorer(),
};
describe("AddMaterializedViewPanel", () => {
it("render default panel", () => {
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
<AddMaterializedViewPanel {...props} />,
);
expect(wrapper).toMatchSnapshot();
});
it("should render form", () => {
const wrapper: ShallowWrapper<AddMaterializedViewPanelProps, object, Component> = shallow(
<AddMaterializedViewPanel {...props} />,
);
const form = wrapper.find("form").first();
expect(form).toBeDefined();
});
});

View File

@@ -0,0 +1,430 @@
import {
DirectionalHint,
Dropdown,
DropdownMenuItemType,
Icon,
IDropdownOption,
Link,
Separator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { createMaterializedView } from "Common/dataAccess/createMaterializedView";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import * as DataModels from "Contracts/DataModels";
import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { Collection, Database } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
import {
AllPropertiesIndexed,
FullTextPolicyDefault,
getPartitionKey,
isSynapseLinkEnabled,
parseUniqueKeys,
scrollToSection,
shouldShowAnalyticalStoreOptions,
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import {
chooseSourceContainerStyle,
chooseSourceContainerStyles,
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanelStyles";
import { AddMVAdvancedComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent";
import { AddMVAnalyticalStoreComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent";
import { AddMVFullTextSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent";
import { AddMVPartitionKeyComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVPartitionKeyComponent";
import { AddMVThroughputComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent";
import { AddMVUniqueKeysComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVUniqueKeysComponent";
import { AddMVVectorSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVVectorSearchComponent";
import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent";
import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent";
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import React, { useEffect, useState } from "react";
import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
export interface AddMaterializedViewPanelProps {
explorer: Explorer;
sourceContainer?: Collection;
}
export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): JSX.Element => {
const { explorer, sourceContainer } = props;
const [sourceContainerOptions, setSourceContainerOptions] = useState<IDropdownOption[]>();
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
const [materializedViewId, setMaterializedViewId] = useState<string>();
const [definition, setDefinition] = useState<string>();
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
const [useHashV1, setUseHashV1] = useState<boolean>();
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>();
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>();
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>();
const [errorMessage, setErrorMessage] = useState<string>();
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
const [isExecuting, setIsExecuting] = useState<boolean>();
useEffect(() => {
const sourceContainerOptions: IDropdownOption[] = [];
useDatabases.getState().databases.forEach((database: Database) => {
sourceContainerOptions.push({
key: database.rid,
text: database.id(),
itemType: DropdownMenuItemType.Header,
});
database.collections().forEach((collection: Collection) => {
const isMaterializedView: boolean = !!collection.materializedViewDefinition();
sourceContainerOptions.push({
key: collection.rid,
text: collection.id(),
disabled: isMaterializedView,
...(isMaterializedView && {
title: "This is a materialized view.",
}),
data: collection,
});
});
});
setSourceContainerOptions(sourceContainerOptions);
}, []);
useEffect(() => {
scrollToSection("panelContainer");
}, [errorMessage]);
let materializedViewThroughput: number;
let isMaterializedViewAutoscale: boolean;
let isCostAcknowledged: boolean;
const materializedViewThroughputOnChange = (materializedViewThroughputValue: number): void => {
materializedViewThroughput = materializedViewThroughputValue;
};
const isMaterializedViewAutoscaleOnChange = (isMaterializedViewAutoscaleValue: boolean): void => {
isMaterializedViewAutoscale = isMaterializedViewAutoscaleValue;
};
const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => {
isCostAcknowledged = isCostAcknowledgedValue;
};
const isSelectedSourceContainerSharedThroughput = (): boolean => {
if (!selectedSourceContainer) {
return false;
}
return !!selectedSourceContainer.getDatabase().offer();
};
const showCollectionThroughputInput = (): boolean => {
if (isServerlessAccount()) {
return false;
}
if (enableDedicatedThroughput) {
return true;
}
return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput();
};
const showVectorSearchParameters = (): boolean => {
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const showFullTextSearchParameters = (): boolean => {
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
};
const getAnalyticalStorageTtl = (): number => {
if (!isSynapseLinkEnabled()) {
return undefined;
}
if (!shouldShowAnalyticalStoreOptions()) {
return undefined;
}
if (enableAnalyticalStore) {
// TODO: always default to 90 days once the backend hotfix is deployed
return userContext.features.ttl90Days
? Constants.AnalyticalStorageTtl.Days90
: Constants.AnalyticalStorageTtl.Infinite;
}
return Constants.AnalyticalStorageTtl.Disabled;
};
const validateInputs = (): boolean => {
if (!selectedSourceContainer) {
setErrorMessage("Please select a source container");
return false;
}
if (materializedViewThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage = isMaterializedViewAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
setErrorMessage(errorMessage);
return false;
}
if (materializedViewThroughput > CollectionCreation.MaxRUPerPartition) {
setErrorMessage("Unsharded collections support up to 10,000 RUs");
return false;
}
if (showVectorSearchParameters()) {
if (!vectorPolicyValidated) {
setErrorMessage("Please fix errors in container vector policy");
return false;
}
if (!fullTextPolicyValidated) {
setErrorMessage("Please fix errors in container full text search policy");
return false;
}
}
return true;
};
const submit = async (event?: React.FormEvent<HTMLFormElement>): Promise<void> => {
event?.preventDefault();
if (!validateInputs()) {
return;
}
const materializedViewIdTrimmed: string = materializedViewId.trim();
const materializedViewDefinition: DataModels.MaterializedViewDefinition = {
sourceCollectionId: sourceContainer.id(),
definition: definition,
};
const partitionKeyTrimmed: string = partitionKey.trim();
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys);
const partitionKeyVersion = useHashV1 ? undefined : 2;
const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed
? {
paths: [
partitionKeyTrimmed,
...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []),
],
kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
version: partitionKeyVersion,
}
: undefined;
const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed;
let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy;
if (showVectorSearchParameters()) {
indexingPolicy.vectorIndexes = vectorIndexingPolicy;
vectorEmbeddingPolicyFinal = {
vectorEmbeddings: vectorEmbeddingPolicy,
};
}
if (showFullTextSearchParameters()) {
indexingPolicy.fullTextIndexes = fullTextIndexes;
}
const telemetryData: TelemetryProcessor.TelemetryData = {
database: {
id: selectedSourceContainer.databaseId,
shared: isSelectedSourceContainerSharedThroughput(),
},
collection: {
id: materializedViewIdTrimmed,
throughput: materializedViewThroughput,
isAutoscale: isMaterializedViewAutoscale,
partitionKeyPaths,
uniqueKeyPolicy,
collectionWithDedicatedThroughput: enableDedicatedThroughput,
},
subscriptionQuotaId: userContext.quotaId,
dataExplorerArea: Constants.Areas.ContextualPane,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData);
const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput;
let offerThroughput: number;
let autoPilotMaxThroughput: number;
if (!databaseLevelThroughput) {
if (isMaterializedViewAutoscale) {
autoPilotMaxThroughput = materializedViewThroughput;
} else {
offerThroughput = materializedViewThroughput;
}
}
const createMaterializedViewParams: DataModels.CreateMaterializedViewsParams = {
materializedViewId: materializedViewIdTrimmed,
materializedViewDefinition: materializedViewDefinition,
databaseId: selectedSourceContainer.databaseId,
databaseLevelThroughput: databaseLevelThroughput,
offerThroughput: offerThroughput,
autoPilotMaxThroughput: autoPilotMaxThroughput,
analyticalStorageTtl: getAnalyticalStorageTtl(),
indexingPolicy: indexingPolicy,
partitionKey: partitionKeyPaths,
uniqueKeyPolicy: uniqueKeyPolicy,
vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal,
fullTextPolicy: fullTextPolicy,
};
setIsExecuting(true);
try {
await createMaterializedView(createMaterializedViewParams);
await explorer.refreshAllDatabases();
TelemetryProcessor.traceSuccess(Action.CreateMaterializedView, telemetryData, startKey);
useSidePanel.getState().closeSidePanel();
} catch (error) {
const errorMessage: string = getErrorMessage(error);
setErrorMessage(errorMessage);
setShowErrorDetails(true);
const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) };
TelemetryProcessor.traceFailure(Action.CreateMaterializedView, failureTelemetryData, startKey);
} finally {
setIsExecuting(false);
}
};
return (
<form className="panelFormWrapper" id="panelMaterializedView" onSubmit={submit}>
{errorMessage && (
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
)}
<div className="panelMainContent">
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Source container id
</Text>
</Stack>
<Dropdown
placeholder="Choose source container"
options={sourceContainerOptions}
defaultSelectedKey={sourceContainer?.rid}
styles={chooseSourceContainerStyles()}
style={chooseSourceContainerStyle()}
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
/>
<Separator className="panelSeparator" />
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
View container id
</Text>
</Stack>
<input
id="materializedViewId"
type="text"
aria-required
required
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder={`e.g., viewByEmailId`}
size={40}
className="panelTextField"
value={materializedViewId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setMaterializedViewId(event.target.value)}
/>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Materialized View Definition
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
Learn more about defining materialized views.
</Link>
}
>
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
</TooltipHost>
</Stack>
<input
id="materializedViewDefinition"
type="text"
aria-required
required
autoComplete="off"
placeholder={"SELECT c.email, c.accountId FROM c"}
size={40}
className="panelTextField"
value={definition || ""}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
/>
<AddMVPartitionKeyComponent
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
/>
<AddMVThroughputComponent
{...{
enableDedicatedThroughput,
setEnabledDedicatedThroughput,
isSelectedSourceContainerSharedThroughput,
showCollectionThroughputInput,
materializedViewThroughputOnChange,
isMaterializedViewAutoscaleOnChange,
setIsThroughputCapExceeded,
isCostAknowledgedOnChange,
}}
/>
<AddMVUniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
{shouldShowAnalyticalStoreOptions() && (
<AddMVAnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
)}
{showVectorSearchParameters() && (
<AddMVVectorSearchComponent
{...{
vectorEmbeddingPolicy,
setVectorEmbeddingPolicy,
vectorIndexingPolicy,
setVectorIndexingPolicy,
vectorPolicyValidated,
setVectorPolicyValidated,
}}
/>
)}
{showFullTextSearchParameters() && (
<AddMVFullTextSearchComponent
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
/>
)}
<AddMVAdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
</Stack>
</div>
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
{isExecuting && <PanelLoadingScreen />}
</form>
);
};

View File

@@ -0,0 +1,15 @@
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
import { CSSProperties } from "react";
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
return {
title: { height: 27, lineHeight: 27 },
dropdownItem: { fontSize: 12 },
dropdownItemDisabled: { fontSize: 12 },
dropdownItemSelected: { fontSize: 12 },
};
}
export function chooseSourceContainerStyle(): CSSProperties {
return { width: 300, fontSize: 12 };
}

View File

@@ -0,0 +1,190 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddMaterializedViewPanel render default panel 1`] = `
<form
className="panelFormWrapper"
id="panelMaterializedView"
onSubmit={[Function]}
>
<div
className="panelMainContent"
>
<Stack>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Source container id
</Text>
</Stack>
<Dropdown
onChange={[Function]}
placeholder="Choose source container"
style={
{
"fontSize": 12,
"width": 300,
}
}
styles={
{
"dropdownItem": {
"fontSize": 12,
},
"dropdownItemDisabled": {
"fontSize": 12,
},
"dropdownItemSelected": {
"fontSize": 12,
},
"title": {
"height": 27,
"lineHeight": 27,
},
}
}
/>
<Separator
className="panelSeparator"
/>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
View container id
</Text>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="materializedViewId"
onChange={[Function]}
pattern="[^/?#\\\\]*[^/?# \\\\]"
placeholder="e.g., viewByEmailId"
required={true}
size={40}
title="May not end with space nor contain characters '\\' '/' '#' '?'"
type="text"
/>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
* 
</span>
<Text
className="panelTextBold"
variant="small"
>
Materialized View Definition
</Text>
<StyledTooltipHostBase
content={
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
Learn more about defining materialized views.
</StyledLinkBase>
}
directionalHint={4}
>
<Icon
className="panelInfoIcon"
iconName="Info"
role="button"
tabIndex={0}
/>
</StyledTooltipHostBase>
</Stack>
<input
aria-required={true}
autoComplete="off"
className="panelTextField"
id="materializedViewDefinition"
onChange={[Function]}
placeholder="SELECT c.email, c.accountId FROM c"
required={true}
size={40}
type="text"
value=""
/>
<AddMVPartitionKeyComponent
partitionKey=""
setPartitionKey={[Function]}
setSubPartitionKeys={[Function]}
subPartitionKeys={[]}
/>
<AddMVThroughputComponent
isCostAknowledgedOnChange={[Function]}
isMaterializedViewAutoscaleOnChange={[Function]}
isSelectedSourceContainerSharedThroughput={[Function]}
materializedViewThroughputOnChange={[Function]}
setEnabledDedicatedThroughput={[Function]}
setIsThroughputCapExceeded={[Function]}
showCollectionThroughputInput={[Function]}
/>
<AddMVUniqueKeysComponent
setUniqueKeys={[Function]}
uniqueKeys={[]}
/>
<AddMVAnalyticalStoreComponent
explorer={
Explorer {
"_isInitializingNotebooks": false,
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
setEnableAnalyticalStore={[Function]}
/>
<AddMVAdvancedComponent
setSubPartitionKeys={[Function]}
setUseHashV1={[Function]}
/>
</Stack>
</div>
<PanelFooterComponent
buttonLabel="OK"
/>
</form>
`;

View File

@@ -94,6 +94,7 @@
padding-left: @MediumSpace; padding-left: @MediumSpace;
.paneErrorLink { .paneErrorLink {
color: @LinkColor;
cursor: pointer; cursor: pointer;
font-size: @mediumFontSize; font-size: @mediumFontSize;
} }

View File

@@ -32,6 +32,7 @@ import {
} from "Shared/StorageUtility"; } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility"; import * as StringUtility from "Shared/StringUtility";
import { updateUserContext, userContext } from "UserContext"; import { updateUserContext, userContext } from "UserContext";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils";
@@ -183,7 +184,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator; const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
const showEnableEntraIdRbac = const showEnableEntraIdRbac =
userContext.apiType === "SQL" && isDataplaneRbacSupported(userContext.apiType) &&
userContext.authType === AuthType.AAD && userContext.authType === AuthType.AAD &&
configContext.platform !== Platform.Fabric && configContext.platform !== Platform.Fabric &&
!isEmulator; !isEmulator;

View File

@@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator"; import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel"; import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { PromptCard } from "Explorer/QueryCopilot/PromptCard"; import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useCarousel } from "hooks/useCarousel"; import { useCarousel } from "hooks/useCarousel";

View File

@@ -18,7 +18,7 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants"; import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils"; import { handleError } from "Common/ErrorHandlingUtils";
import QueryError, { QueryErrorSeverity } from "Common/QueryError"; import QueryError, { QueryErrorSeverity } from "Common/QueryError";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
@@ -393,8 +393,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}, },
}} }}
disabled={isGeneratingQuery} disabled={isGeneratingQuery}
autoComplete="list" autoComplete="off"
aria-expanded={showSamplePrompts}
placeholder="Ask a question in natural language and well generate the query for you." placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label" aria-labelledby="copilot-textfield-label"
onRenderSuffix={() => { onRenderSuffix={() => {
@@ -580,7 +579,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}> <Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
{userContext.feedbackPolicies?.policyAllowFeedback && ( {userContext.feedbackPolicies?.policyAllowFeedback && (
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<Text style={{ fontSize: 12 }}>Provide feedback</Text> <Text style={{ fontSize: 12 }}>{FeedbackLabels.provideFeedback}</Text>
{showCallout && !hideFeedbackModalForLikedQueries && ( {showCallout && !hideFeedbackModalForLikedQueries && (
<Callout <Callout
role="status" role="status"
@@ -630,8 +629,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
<IconButton <IconButton
id="likeBtn" id="likeBtn"
style={{ marginLeft: 10 }} style={{ marginLeft: 10 }}
aria-label="Like" aria-label={FeedbackLabels.provideFeedback}
role="toggle" role="button"
title="Like"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }} iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => { onClick={() => {
setShowCallout(!likeQuery); setShowCallout(!likeQuery);
@@ -649,8 +649,9 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
/> />
<IconButton <IconButton
style={{ margin: "0 4px" }} style={{ margin: "0 4px" }}
role="toggle" role="button"
aria-label="Dislike" aria-label={FeedbackLabels.provideFeedback}
title="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }} iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => { onClick={() => {
let toggleStatusValue = "Unpressed"; let toggleStatusValue = "Unpressed";

View File

@@ -1,7 +1,6 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { import {
Areas, Areas,
BackendApi,
ConnectionStatusType, ConnectionStatusType,
ContainerStatusType, ContainerStatusType,
HttpStatusCodes, HttpStatusCodes,
@@ -32,7 +31,6 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import { queryPagesUntilContentPresent } from "Utils/QueryUtils"; import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@@ -82,9 +80,7 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
}; };
export const getCopilotEnabled = async (): Promise<boolean> => { export const getCopilotEnabled = async (): Promise<boolean> => {
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings) const backendEndpoint: string = configContext.PORTAL_BACKEND_ENDPOINT;
? configContext.PORTAL_BACKEND_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const url = `${backendEndpoint}/api/portalsettings/querycopilot`; const url = `${backendEndpoint}/api/portalsettings/querycopilot`;
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();

View File

@@ -1,5 +1,6 @@
import { import {
Button, Button,
makeStyles,
Menu, Menu,
MenuButton, MenuButton,
MenuButtonProps, MenuButtonProps,
@@ -7,20 +8,26 @@ import {
MenuList, MenuList,
MenuPopover, MenuPopover,
MenuTrigger, MenuTrigger,
SplitButton,
makeStyles,
mergeClasses, mergeClasses,
shorthands, shorthands,
SplitButton,
} from "@fluentui/react-components"; } from "@fluentui/react-components";
import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons"; import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons";
import { Platform, configContext } from "ConfigContext"; import { MaterializedViewsLabels } from "Common/Constants";
import { isMaterializedViewsEnabled } from "Common/DatabaseAccountUtility";
import { configContext, Platform } from "ConfigContext";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel"; import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel";
import {
AddMaterializedViewPanel,
AddMaterializedViewPanelProps,
} from "Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel";
import { Tabs } from "Explorer/Tabs/Tabs"; import { Tabs } from "Explorer/Tabs/Tabs";
import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
import { ResourceTree } from "Explorer/Tree/ResourceTree"; import { ResourceTree } from "Explorer/Tree/ResourceTree";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
import { Allotment, AllotmentHandle } from "allotment"; import { Allotment, AllotmentHandle } from "allotment";
@@ -123,7 +130,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
const actions = useMemo<GlobalCommand[]>(() => { const actions = useMemo<GlobalCommand[]>(() => {
if ( if (
configContext.platform === Platform.Fabric || (isFabric() && userContext.fabricContext?.isReadOnly) ||
userContext.apiType === "Postgres" || userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo" userContext.apiType === "VCoreMongo"
) { ) {
@@ -137,12 +144,15 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
id: "new_collection", id: "new_collection",
label: `New ${getCollectionName()}`, label: `New ${getCollectionName()}`,
icon: <Add16Regular />, icon: <Add16Regular />,
onClick: () => explorer.onNewCollectionClicked(), onClick: () => {
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
explorer.onNewCollectionClicked({ databaseId });
},
keyboardAction: KeyboardAction.NEW_COLLECTION, keyboardAction: KeyboardAction.NEW_COLLECTION,
}, },
]; ];
if (userContext.apiType !== "Tables") { if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") {
actions.push({ actions.push({
id: "new_database", id: "new_database",
label: `New ${getDatabaseName()}`, label: `New ${getDatabaseName()}`,
@@ -158,6 +168,25 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
}); });
} }
if (isMaterializedViewsEnabled()) {
const addMaterializedViewPanelProps: AddMaterializedViewPanelProps = {
explorer,
};
actions.push({
id: "new_materialized_view",
label: MaterializedViewsLabels.NewMaterializedView,
icon: <Add16Regular />,
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
MaterializedViewsLabels.NewMaterializedView,
<AddMaterializedViewPanel {...addMaterializedViewPanelProps} />,
),
});
}
return actions; return actions;
}, [explorer]); }, [explorer]);
@@ -288,7 +317,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
}, [setLoading]); }, [setLoading]);
const hasGlobalCommands = !( const hasGlobalCommands = !(
configContext.platform === Platform.Fabric || isFabricMirrored() ||
userContext.apiType === "Postgres" || userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo" userContext.apiType === "VCoreMongo"
); );

View 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>
);
};

View File

@@ -3,7 +3,7 @@ import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { CassandraProxyAPIs, CassandraProxyEndpoints } from "../../Common/Constants"; import { CassandraProxyAPIs } from "../../Common/Constants";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/dataAccess/createDocument";
@@ -264,9 +264,6 @@ export class CassandraAPIDataClient extends TableDataClient {
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string, paginationToken?: string,
): Promise<Entities.IListTableEntitiesResult> { ): Promise<Entities.IListTableEntitiesResult> {
if (!this.useCassandraProxyEndpoint("postQuery")) {
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
}
const clearMessage = const clearMessage =
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
try { 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( public async deleteDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[], entitiesToDelete: Entities.ITableEntity[],
@@ -471,10 +419,6 @@ export class CassandraAPIDataClient extends TableDataClient {
} }
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> { public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
if (!this.useCassandraProxyEndpoint("getKeys")) {
return this.getTableKeys_ToBeDeprecated(collection);
}
if (!!collection.cassandraKeys) { if (!!collection.cassandraKeys) {
return Q.resolve(collection.cassandraKeys); return Q.resolve(collection.cassandraKeys);
} }
@@ -515,52 +459,7 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; 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[]> { public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
if (!this.useCassandraProxyEndpoint("getSchema")) {
return this.getTableSchema_ToBeDeprecated(collection);
}
if (!!collection.cassandraSchema) { if (!!collection.cassandraSchema) {
return Q.resolve(collection.cassandraSchema); return Q.resolve(collection.cassandraSchema);
} }
@@ -602,52 +501,7 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; 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> { 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 deferred = Q.defer();
const { authType, databaseAccount } = userContext; const { authType, databaseAccount } = userContext;
const apiEndpoint: string = const apiEndpoint: string =
@@ -677,38 +531,6 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; 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 { private trimCassandraEndpoint(cassandraEndpoint: string): string {
if (!cassandraEndpoint) { if (!cassandraEndpoint) {
return cassandraEndpoint; return cassandraEndpoint;
@@ -747,23 +569,4 @@ export class CassandraAPIDataClient extends TableDataClient {
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string { private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
return collection.cassandraKeys.partitionKeys[0].property; 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)
);
}
} }

View File

@@ -2,6 +2,7 @@ import { FeedResponse, ItemDefinition, Resource } from "@azure/cosmos";
import { waitFor } from "@testing-library/react"; import { waitFor } from "@testing-library/react";
import { deleteDocuments } from "Common/dataAccess/deleteDocument"; import { deleteDocuments } from "Common/dataAccess/deleteDocument";
import { Platform, updateConfigContext } from "ConfigContext"; import { Platform, updateConfigContext } from "ConfigContext";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact"; import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
@@ -341,10 +342,15 @@ describe("Documents tab (noSql API)", () => {
updateConfigContext({ platform: Platform.Fabric }); updateConfigContext({ platform: Platform.Fabric });
updateUserContext({ updateUserContext({
fabricContext: { fabricContext: {
connectionId: "test", databaseName: "database",
databaseConnectionInfo: undefined, artifactInfo: {
connectionId: "test",
resourceTokenInfo: undefined,
},
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
isReadOnly: true, isReadOnly: true,
isVisible: true, isVisible: true,
fabricClientRpcVersion: "rpcVersion",
}, },
}); });

View File

@@ -20,7 +20,6 @@ import {
import { queryDocuments } from "Common/dataAccess/queryDocuments"; import { queryDocuments } from "Common/dataAccess/queryDocuments";
import { readDocument } from "Common/dataAccess/readDocument"; import { readDocument } from "Common/dataAccess/readDocument";
import { updateDocument } from "Common/dataAccess/updateDocument"; import { updateDocument } from "Common/dataAccess/updateDocument";
import { Platform, configContext } from "ConfigContext";
import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts"; import { ActionType, OpenCollectionTab, TabKind } from "Contracts/ActionContracts";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "Explorer/Controls/Dialog"; 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 { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { QueryConstants } from "Shared/Constants"; import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -344,7 +344,7 @@ export const getTabsButtons = ({
onRevertExistingDocumentClick, onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick, onDeleteExistingDocumentsClick,
}: ButtonsDependencies): CommandButtonComponentProps[] => { }: ButtonsDependencies): CommandButtonComponentProps[] => {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { if (isFabric() && userContext.fabricContext?.isReadOnly) {
// All the following buttons require write access // All the following buttons require write access
return []; return [];
} }
@@ -1150,27 +1150,16 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds); deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
} }
} else { } else {
if (isMongoBulkDeleteDisabled) { deletePromise = MongoProxyClient.deleteDocuments(
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument(). _collection.databaseId,
// MongoProxyClient.deleteDocuments() should be called for all users. _collection as ViewModels.Collection,
deletePromise = MongoProxyClient.deleteDocument( toDeleteDocumentIds,
_collection.databaseId, ).then(({ deletedCount, isAcknowledged }) => {
_collection as ViewModels.Collection, if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
toDeleteDocumentIds[0], return toDeleteDocumentIds;
).then(() => [toDeleteDocumentIds[0]]); }
// ---------------------------------------------------------------------------------------------------- throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
} 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}`);
});
}
} }
return deletePromise return deletePromise
@@ -2054,11 +2043,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
} }
}, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]); }, [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 // TODO: remove partitionKey.systemKey when JS SDK bug is fixed
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete); const isBulkDeleteDisabled = partitionKey.systemKey && !isPreferredApiMongoDB;
const isBulkDeleteDisabled =
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
// ------------------------------------------------------- // -------------------------------------------------------
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => { const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
@@ -2150,8 +2136,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedColumnIds={selectedColumnIds} selectedColumnIds={selectedColumnIds}
columnDefinitions={columnDefinitions} columnDefinitions={columnDefinitions}
isRowSelectionDisabled={ isRowSelectionDisabled={
isBulkDeleteDisabled || isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
} }
onColumnSelectionChange={onColumnSelectionChange} onColumnSelectionChange={onColumnSelectionChange}
defaultColumnSelection={getInitialColumnSelection()} defaultColumnSelection={getInitialColumnSelection()}

View File

@@ -1,6 +1,4 @@
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
import React, { Component } from "react"; import React, { Component } from "react";
import * as Constants from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext"; import { configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
@@ -50,15 +48,13 @@ export default class MongoShellTabComponent extends Component<
IMongoShellTabComponentStates IMongoShellTabComponentStates
> { > {
private _logTraces: Map<string, number>; private _logTraces: Map<string, number>;
private _useMongoProxyEndpoint: boolean;
constructor(props: IMongoShellTabComponentProps) { constructor(props: IMongoShellTabComponentProps) {
super(props); super(props);
this._logTraces = new Map(); this._logTraces = new Map();
this._useMongoProxyEndpoint = useMongoProxyEndpoint(Constants.MongoProxyApi.LegacyMongoShell);
this.state = { this.state = {
url: getMongoShellUrl(this._useMongoProxyEndpoint), url: getMongoShellUrl(),
}; };
props.onMongoShellTabAccessor({ props.onMongoShellTabAccessor({
@@ -113,17 +109,9 @@ export default class MongoShellTabComponent extends Component<
const resourceId = databaseAccount?.id; const resourceId = databaseAccount?.id;
const accountName = databaseAccount?.name; const accountName = databaseAccount?.name;
const documentEndpoint = databaseAccount?.properties.mongoEndpoint || databaseAccount?.properties.documentEndpoint; 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 databaseId = this.props.collection.databaseId;
const collectionId = this.props.collection.id(); const collectionId = this.props.collection.id();
const apiEndpoint = this._useMongoProxyEndpoint const apiEndpoint = configContext.MONGO_PROXY_ENDPOINT;
? configContext.MONGO_PROXY_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const encryptedAuthToken: string = userContext.accessToken; const encryptedAuthToken: string = userContext.accessToken;
shellIframe.contentWindow.postMessage( shellIframe.contentWindow.postMessage(
@@ -132,7 +120,7 @@ export default class MongoShellTabComponent extends Component<
data: { data: {
resourceId: resourceId, resourceId: resourceId,
accountName: accountName, accountName: accountName,
mongoEndpoint: this._useMongoProxyEndpoint ? documentEndpoint : mongoEndpoint, mongoEndpoint: documentEndpoint,
authorization: authorization, authorization: authorization,
databaseId: databaseId, databaseId: databaseId,
collectionId: collectionId, collectionId: collectionId,

View File

@@ -2,8 +2,6 @@ import { Platform, resetConfigContext, updateConfigContext } from "../../../Conf
import { updateUserContext, userContext } from "../../../UserContext"; import { updateUserContext, userContext } from "../../../UserContext";
import { getMongoShellUrl } from "./getMongoShellUrl"; import { getMongoShellUrl } from "./getMongoShellUrl";
const mongoBackendEndpoint = "https://localhost:1234";
describe("getMongoShellUrl", () => { describe("getMongoShellUrl", () => {
let queryString = ""; let queryString = "";
@@ -11,7 +9,6 @@ describe("getMongoShellUrl", () => {
resetConfigContext(); resetConfigContext();
updateConfigContext({ updateConfigContext({
BACKEND_ENDPOINT: mongoBackendEndpoint,
platform: Platform.Hosted, platform: Platform.Hosted,
}); });
@@ -37,12 +34,7 @@ describe("getMongoShellUrl", () => {
queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`; queryString = `resourceId=${userContext.databaseAccount.id}&accountName=${userContext.databaseAccount.name}&mongoEndpoint=${userContext.databaseAccount.properties.documentEndpoint}`;
}); });
it("should return /indexv2.html by default", () => { it("should return /index.html by default", () => {
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`); expect(getMongoShellUrl().toString()).toContain(`/index.html?${queryString}`);
});
it("should return /index.html when useMongoProxyEndpoint is true", () => {
const useMongoProxyEndpoint: boolean = true;
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
}); });
}); });

View File

@@ -1,11 +1,11 @@
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string { export function getMongoShellUrl(): string {
const { databaseAccount: account } = userContext; const { databaseAccount: account } = userContext;
const resourceId = account?.id; const resourceId = account?.id;
const accountName = account?.name; const accountName = account?.name;
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`; return `/mongoshell/index.html?${queryString}`;
} }

View File

@@ -375,6 +375,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
ruCapPerOperation: ruThreshold, ruCapPerOperation: ruThreshold,
} as QueryOperationOptions; } as QueryOperationOptions;
} }
const queryDocuments = async (firstItemIndex: number) => const queryDocuments = async (firstItemIndex: number) =>
await queryDocumentsPage( await queryDocumentsPage(
this.props.collection && this.props.collection.id(), this.props.collection && this.props.collection.id(),

View File

@@ -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 { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab"; import { QueryCopilotTab } from "Explorer/QueryCopilot/QueryCopilotTab";
import { FabricHomeScreen } from "Explorer/SplashScreen/FabricHome";
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
import { ConnectTab } from "Explorer/Tabs/ConnectTab"; import { ConnectTab } from "Explorer/Tabs/ConnectTab";
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab"; import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab"; import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab"; import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab"; import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { LayoutConstants } from "Explorer/Theme/ThemeUtil";
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout"; import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
@@ -34,10 +30,6 @@ interface TabsProps {
export const Tabs = ({ explorer }: TabsProps): JSX.Element => { export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs(); const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
const [
showMongoAndCassandraProxiesNetworkSettingsWarningState,
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS); const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.TABS);
useEffect(() => { useEffect(() => {
@@ -48,28 +40,8 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
}); });
}, [setKeyboardHandlers]); }, [setKeyboardHandlers]);
const defaultMessageBarStyles: IMessageBarStyles = {
root: {
height: `${LayoutConstants.rowHeight}px`,
overflow: "hidden",
flexDirection: "row",
},
};
return ( return (
<div className="tabsManagerContainer"> <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"> <div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist"> <ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{openedReactTabs.map((tab) => ( {openedReactTabs.map((tab) => (
@@ -301,7 +273,11 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
<ConnectTab /> <ConnectTab />
); );
case ReactTabKind.Home: case ReactTabKind.Home:
return <SplashScreen explorer={explorer} />; if (isFabricNative()) {
return <FabricHomeScreen explorer={explorer} />;
} else {
return <SplashScreen explorer={explorer} />;
}
case ReactTabKind.Quickstart: case ReactTabKind.Quickstart:
return userContext.apiType === "VCoreMongo" ? ( return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoQuickstartTab explorer={explorer} /> <VcoreMongoQuickstartTab explorer={explorer} />
@@ -314,57 +290,3 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`); throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
} }
}; };
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if (
((userContext.apiType === "Mongo" && 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;
};

View File

@@ -1,6 +1,7 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook"; import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore"; import * as _ from "underscore";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@@ -34,7 +35,6 @@ import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
import { Platform, configContext } from "./../../ConfigContext";
import ConflictId from "./ConflictId"; import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure"; import StoredProcedure from "./StoredProcedure";
@@ -58,6 +58,8 @@ export default class Collection implements ViewModels.Collection {
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public usageSizeInKB: ko.Observable<number>; public usageSizeInKB: ko.Observable<number>;
public computedProperties: ko.Observable<DataModels.ComputedProperties>; public computedProperties: ko.Observable<DataModels.ComputedProperties>;
public materializedViews: ko.Observable<DataModels.MaterializedView[]>;
public materializedViewDefinition: ko.Observable<DataModels.MaterializedViewDefinition>;
public offer: ko.Observable<DataModels.Offer>; public offer: ko.Observable<DataModels.Offer>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>; public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
@@ -124,6 +126,8 @@ export default class Collection implements ViewModels.Collection {
this.requestSchema = data.requestSchema; this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig); this.geospatialConfig = ko.observable(data.geospatialConfig);
this.computedProperties = ko.observable(data.computedProperties); this.computedProperties = ko.observable(data.computedProperties);
this.materializedViews = ko.observable(data.materializedViews);
this.materializedViewDefinition = ko.observable(data.materializedViewDefinition);
this.partitionKeyPropertyHeaders = this.partitionKey?.paths; this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => { this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
@@ -210,7 +214,7 @@ export default class Collection implements ViewModels.Collection {
}); });
const showScriptsMenus: boolean = 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.showStoredProcedures = ko.observable<boolean>(showScriptsMenus);
this.showTriggers = ko.observable<boolean>(showScriptsMenus); this.showTriggers = ko.observable<boolean>(showScriptsMenus);
this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus); this.showUserDefinedFunctions = ko.observable<boolean>(showScriptsMenus);

View File

@@ -19,7 +19,7 @@ import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { AddCollectionPanel } from "../Panes/AddCollectionPanel"; import { AddCollectionPanel } from "../Panes/AddCollectionPanel/AddCollectionPanel";
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";

View File

@@ -1,7 +1,7 @@
import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
import { Home16Regular } from "@fluentui/react-icons"; import { Home16Regular } from "@fluentui/react-icons";
import { AuthType } from "AuthType"; import { AuthType } from "AuthType";
import { Platform, configContext } from "ConfigContext"; import { Collection } from "Contracts/ViewModels";
import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles";
import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { import {
@@ -11,6 +11,7 @@ import {
} from "Explorer/Tree/treeNodeUtil"; } from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -60,7 +61,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
const databaseTreeNodes = useMemo(() => { const databaseTreeNodes = useMemo(() => {
return userContext.authType === AuthType.ResourceToken return userContext.authType === AuthType.ResourceToken
? createResourceTokenTreeNodes(resourceTokenCollection) ? createResourceTokenTreeNodes(resourceTokenCollection as Collection)
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab); : createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]); }, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
@@ -76,23 +77,22 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
: []; : [];
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]); }, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
const headerNodes: TreeNode[] = const headerNodes: TreeNode[] = isFabricMirrored()
configContext.platform === Platform.Fabric ? []
? [] : [
: [ {
{ id: "home",
id: "home", iconSrc: <Home16Regular />,
iconSrc: <Home16Regular />, label: "Home",
label: "Home", isSelected: () =>
isSelected: () => useSelectedNode.getState().selectedNode === undefined &&
useSelectedNode.getState().selectedNode === undefined && useTabs.getState().activeReactTab === ReactTabKind.Home,
useTabs.getState().activeReactTab === ReactTabKind.Home, onClick: () => {
onClick: () => { useSelectedNode.getState().setSelectedNode(undefined);
useSelectedNode.getState().setSelectedNode(undefined); useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
},
}, },
]; },
];
const rootNodes: TreeNode[] = useMemo(() => { const rootNodes: TreeNode[] = useMemo(() => {
if (sampleDataNodes.length > 0) { if (sampleDataNodes.length > 0) {

View File

@@ -30,7 +30,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -72,7 +72,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -145,7 +145,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -264,7 +264,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -369,7 +369,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -442,7 +442,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -546,7 +546,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -696,7 +696,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -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": [ "children": [
@@ -753,8 +753,14 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -774,8 +780,14 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -822,8 +834,14 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -870,8 +888,14 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"label": "New SQL Query", "label": "New SQL Query",
"onClick": [Function], "onClick": [Function],
}, },
{
"iconSrc": {},
"label": "Delete Container",
"onClick": [Function],
"styleClass": "deleteCollectionMenuItem",
},
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -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": <EyeRegular
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": <EyeRegular
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": <EyeRegular
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": <EyeRegular
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`] = ` 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], "isSelected": [Function],
"label": "mockSproc3", "label": "mockSproc4",
"onClick": [Function], "onClick": [Function],
}, },
], ],
@@ -990,7 +1153,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockUdf3", "label": "mockUdf4",
"onClick": [Function], "onClick": [Function],
}, },
], ],
@@ -1008,7 +1171,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
}, },
], ],
"isSelected": [Function], "isSelected": [Function],
"label": "mockTrigger3", "label": "mockTrigger4",
"onClick": [Function], "onClick": [Function],
}, },
], ],
@@ -1045,7 +1208,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -1148,7 +1311,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -1282,7 +1445,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -1462,7 +1625,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -1636,7 +1799,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -1734,7 +1897,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -1868,7 +2031,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -2048,7 +2211,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
"styleClass": "deleteCollectionMenuItem", "styleClass": "deleteCollectionMenuItem",
}, },
], ],
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,
@@ -2103,7 +2266,7 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
}, },
], ],
"className": "collectionNode", "className": "collectionNode",
"iconSrc": <DocumentMultipleRegular "iconSrc": <EyeRegular
fontSize={16} fontSize={16}
/>, />,
"isExpanded": true, "isExpanded": true,

View File

@@ -1,5 +1,6 @@
import { CapabilityNames } from "Common/Constants"; import { CapabilityNames } from "Common/Constants";
import { Platform, updateConfigContext } from "ConfigContext"; import { Platform, updateConfigContext } from "ConfigContext";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
@@ -16,7 +17,7 @@ import {
} from "Explorer/Tree/treeNodeUtil"; } from "Explorer/Tree/treeNodeUtil";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useSelectedNode } from "Explorer/useSelectedNode"; import { useSelectedNode } from "Explorer/useSelectedNode";
import { updateUserContext } from "UserContext"; import { FabricContext, updateUserContext, UserContext } from "UserContext";
import PromiseSource from "Utils/PromiseSource"; import PromiseSource from "Utils/PromiseSource";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@@ -81,6 +82,7 @@ jest.mock("Explorer/Tree/Trigger", () => {
jest.mock("Common/DatabaseAccountUtility", () => { jest.mock("Common/DatabaseAccountUtility", () => {
return { return {
isPublicInternetAccessAllowed: () => true, isPublicInternetAccessAllowed: () => true,
isMaterializedViewsEnabled: () => false,
}; };
}); });
@@ -133,6 +135,15 @@ const baseCollection = {
kind: "hash", kind: "hash",
version: 2, version: 2,
}, },
materializedViews: ko.observable<DataModels.MaterializedView[]>([
{ id: "view1", _rid: "rid1" },
{ id: "view2", _rid: "rid2" },
]),
materializedViewDefinition: ko.observable<DataModels.MaterializedViewDefinition>({
definition: "SELECT * FROM c WHERE c.id = 1",
sourceCollectionId: "source1",
sourceCollectionRid: "rid123",
}),
storedProcedures: ko.observableArray([]), storedProcedures: ko.observableArray([]),
userDefinedFunctions: ko.observableArray([]), userDefinedFunctions: ko.observableArray([]),
triggers: ko.observableArray([]), triggers: ko.observableArray([]),
@@ -360,9 +371,30 @@ describe("createDatabaseTreeNodes", () => {
}); });
}); });
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>]>([ it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }], [
["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }], "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", "the Cassandra API, serverless, on Hosted",
Platform.Hosted, Platform.Hosted,
@@ -373,6 +405,7 @@ describe("createDatabaseTreeNodes", () => {
{ name: CapabilityNames.EnableServerless, description: "" }, { name: CapabilityNames.EnableServerless, description: "" },
], ],
}, },
{ fabricContext: undefined },
], ],
[ [
"the Mongo API, with Notebooks and Phoenix features, on Emulator", "the Mongo API, with Notebooks and Phoenix features, on Emulator",
@@ -381,26 +414,31 @@ describe("createDatabaseTreeNodes", () => {
{ {
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }], capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
}, },
{ fabricContext: undefined },
], ],
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => { ])(
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled }); "generates the correct tree structure for %s",
updateConfigContext({ platform }); (_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
updateUserContext({ useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
databaseAccount: { updateConfigContext({ platform });
properties: { updateUserContext({
enableMultipleWriteLocations: true, ...userContext,
...dbAccountProperties, databaseAccount: {
}, properties: {
} as unknown as DataModels.DatabaseAccount, enableMultipleWriteLocations: true,
}); ...dbAccountProperties,
const nodes = createDatabaseTreeNodes( },
explorer, } as unknown as DataModels.DatabaseAccount,
isNotebookEnabled, });
useDatabases.getState().databases, const nodes = createDatabaseTreeNodes(
refreshActiveTab, explorer,
); isNotebookEnabled,
expect(nodes).toMatchSnapshot(); 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. // 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. // They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle.
@@ -551,7 +589,18 @@ describe("createDatabaseTreeNodes", () => {
}); });
it.each([ 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", "for Cassandra API",
() => () =>

View File

@@ -1,4 +1,4 @@
import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons"; import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
import TabsBase from "Explorer/Tabs/TabsBase"; import TabsBase from "Explorer/Tabs/TabsBase";
@@ -6,6 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure";
import Trigger from "Explorer/Tree/Trigger"; import Trigger from "Explorer/Tree/Trigger";
import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction"; import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import { isServerlessAccount } from "Utils/CapabilityUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
@@ -22,14 +23,13 @@ import { useNotebook } from "../Notebook/useNotebook";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
export const shouldShowScriptNodes = (): boolean => { export const shouldShowScriptNodes = (): boolean => {
return ( return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
}; };
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />; const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;
const TreeSettingsIcon = <SettingsRegular fontSize={16} />; const TreeSettingsIcon = <SettingsRegular fontSize={16} />;
const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />; const TreeCollectionIcon = <DocumentMultipleRegular fontSize={16} />;
const MaterializedViewCollectionIcon = <EyeRegular fontSize={16} />; //check icon
export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => { export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => {
const updatedSampleTree: TreeNode = { const updatedSampleTree: TreeNode = {
@@ -81,7 +81,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie
return [updatedSampleTree]; return [updatedSampleTree];
}; };
export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => { export const createResourceTokenTreeNodes = (collection: ViewModels.Collection): TreeNode[] => {
if (!collection) { if (!collection) {
return [ return [
{ {
@@ -111,7 +111,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
isExpanded: true, isExpanded: true,
children, children,
className: "collectionNode", className: "collectionNode",
iconSrc: TreeCollectionIcon, iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
onClick: () => { onClick: () => {
// Rewritten version of expandCollapseCollection // Rewritten version of expandCollapseCollection
useSelectedNode.getState().setSelectedNode(collection); useSelectedNode.getState().setSelectedNode(collection);
@@ -229,7 +229,7 @@ export const buildCollectionNode = (
children: children, children: children,
className: "collectionNode", className: "collectionNode",
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection),
iconSrc: TreeCollectionIcon, iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
onClick: () => { onClick: () => {
useSelectedNode.getState().setSelectedNode(collection); useSelectedNode.getState().setSelectedNode(collection);
collection.openTab(); collection.openTab();

View File

@@ -1,56 +1,112 @@
import { sendCachedDataMessage } from "Common/MessageHandler"; import { sendCachedDataMessage } from "Common/MessageHandler";
import { configContext, Platform } from "ConfigContext";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
import { updateUserContext, userContext } from "UserContext"; import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
import { logConsoleError } from "Utils/NotificationConsoleUtils"; import { logConsoleError } from "Utils/NotificationConsoleUtils";
const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe const TOKEN_VALIDITY_MS = (3600 - 600) * 1000; // 1 hour minus 10 minutes to be safe
const DEBOUNCE_DELAY_MS = 1000 * 20; // 20 second 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 // 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()) { if (lastRequestTimestamp !== undefined && lastRequestTimestamp + DEBOUNCE_DELAY_MS > Date.now()) {
return; return;
} }
lastRequestTimestamp = Date.now(); lastRequestTimestamp = Date.now();
try { try {
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>( if (isFabricMirrored()) {
FabricMessageTypes.GetAllResourceTokens, await requestAndStoreDatabaseResourceTokens();
[], } else if (isFabricNative()) {
userContext.fabricContext.connectionId, await requestAndStoreAccessToken();
);
if (!userContext.databaseAccount.properties.documentEndpoint) {
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
} }
updateUserContext({ scheduleRefreshFabricToken();
fabricContext: {
...userContext.fabricContext,
databaseConnectionInfo: fabricDatabaseConnectionInfo,
isReadOnly: true,
},
databaseAccount: { ...userContext.databaseAccount },
});
scheduleRefreshDatabaseResourceToken();
} catch (error) { } catch (error) {
logConsoleError(error); logConsoleError(error as string);
throw error; throw error;
} finally { } finally {
lastRequestTimestamp = undefined; 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 * Check token validity and schedule a refresh if necessary
* @param tokenTimestamp * @param tokenTimestamp
* @returns * @returns
*/ */
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => { export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
return new Promise((resolve) => { return new Promise((resolve) => {
if (timeoutId !== undefined) { if (timeoutId !== undefined) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@@ -59,7 +115,7 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
timeoutId = setTimeout( timeoutId = setTimeout(
() => { () => {
requestDatabaseResourceTokens().then(resolve); requestFabricToken().then(resolve);
}, },
refreshNow ? 0 : TOKEN_VALIDITY_MS, refreshNow ? 0 : TOKEN_VALIDITY_MS,
); );
@@ -68,6 +124,15 @@ export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Prom
export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => { export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): void => {
if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) { if (tokenTimestamp + TOKEN_VALIDITY_MS < Date.now()) {
scheduleRefreshDatabaseResourceToken(true); 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;

View File

@@ -1,13 +1,11 @@
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { useNewPortalBackendEndpoint } from "Utils/EndpointUtils";
import * as React from "react"; import * as React from "react";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import ErrorImage from "../../../../images/error.svg"; import ErrorImage from "../../../../images/error.svg";
import { AuthType } from "../../../AuthType"; import { AuthType } from "../../../AuthType";
import { BackendApi, HttpHeaders } from "../../../Common/Constants"; import { HttpHeaders } from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext"; import { configContext } from "../../../ConfigContext";
import { GenerateTokenResponse } from "../../../Contracts/DataModels";
import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils";
interface Props { interface Props {
@@ -19,10 +17,6 @@ interface Props {
} }
export const fetchEncryptedToken = async (connectionString: string): Promise<string> => { export const fetchEncryptedToken = async (connectionString: string): Promise<string> => {
if (!useNewPortalBackendEndpoint(BackendApi.GenerateToken)) {
return await fetchEncryptedToken_ToBeDeprecated(connectionString);
}
const headers = new Headers(); const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString); headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken"; 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); 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> => { export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise<boolean> => {
const headers = new Headers(); const headers = new Headers();
headers.append(HttpHeaders.connectionString, connectionString); headers.append(HttpHeaders.connectionString, connectionString);
const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.AccountRestrictions)
? configContext.PORTAL_BACKEND_ENDPOINT
: configContext.BACKEND_ENDPOINT;
const url = backendEndpoint + "/api/guest/accountrestrictions/checkconnectionstringlogin";
const response = await fetch(url, { headers, method: "POST" }); const response = await fetch(url, { headers, method: "POST" });
if (!response.ok) { if (!response.ok) {
throw response; throw response;

View File

@@ -16,6 +16,7 @@ export type Features = {
readonly enableAadDataPlane: boolean; readonly enableAadDataPlane: boolean;
readonly enableResourceGraph: boolean; readonly enableResourceGraph: boolean;
readonly enableKoResourceTree: boolean; readonly enableKoResourceTree: boolean;
readonly enableThroughputBuckets: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly phoenixEndpoint?: string; readonly phoenixEndpoint?: string;
@@ -81,6 +82,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableSpark: "true" === get("enablespark"), enableSpark: "true" === get("enablespark"),
enableTtl: "true" === get("enablettl"), enableTtl: "true" === get("enablettl"),
enableKoResourceTree: "true" === get("enablekoresourcetree"), enableKoResourceTree: "true" === get("enablekoresourcetree"),
enableThroughputBuckets: "true" === get("enablethroughputbuckets"),
executeSproc: "true" === get("dataexplorerexecutesproc"), executeSproc: "true" === get("dataexplorerexecutesproc"),
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
mongoProxyEndpoint: get("mongoproxyendpoint"), mongoProxyEndpoint: get("mongoproxyendpoint"),

View File

@@ -41,13 +41,13 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
case SelfServeType.example: { case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample"); const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
const selfServeExample = new SelfServeExample.default(); const selfServeExample = new SelfServeExample.default();
await loadTranslations(selfServeExample.constructor.name); await loadTranslations(selfServeType);
return selfServeExample.toSelfServeDescriptor(); return selfServeExample.toSelfServeDescriptor();
} }
case SelfServeType.sqlx: { case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX"); const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
const sqlX = new SqlX.default(); const sqlX = new SqlX.default();
await loadTranslations(sqlX.constructor.name); await loadTranslations(selfServeType);
return sqlX.toSelfServeDescriptor(); return sqlX.toSelfServeDescriptor();
} }
case SelfServeType.graphapicompute: { case SelfServeType.graphapicompute: {
@@ -55,7 +55,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute" /* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
); );
const graphAPICompute = new GraphAPICompute.default(); const graphAPICompute = new GraphAPICompute.default();
await loadTranslations(graphAPICompute.constructor.name); await loadTranslations(selfServeType);
return graphAPICompute.toSelfServeDescriptor(); return graphAPICompute.toSelfServeDescriptor();
} }
case SelfServeType.materializedviewsbuilder: { case SelfServeType.materializedviewsbuilder: {
@@ -63,7 +63,7 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder" /* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
); );
const materializedViewsBuilder = new MaterializedViewsBuilder.default(); const materializedViewsBuilder = new MaterializedViewsBuilder.default();
await loadTranslations(materializedViewsBuilder.constructor.name); await loadTranslations(selfServeType);
return materializedViewsBuilder.toSelfServeDescriptor(); return materializedViewsBuilder.toSelfServeDescriptor();
} }
default: default:
@@ -103,7 +103,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
const selfServeTypeText = urlSearchParams.get("selfServeType") || inputs.selfServeType; 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 ( if (
!inputs.subscriptionId || !inputs.subscriptionId ||
!inputs.resourceGroup || !inputs.resourceGroup ||

View File

@@ -29,10 +29,11 @@ export enum SelfServeType {
// Unsupported self serve type passed as feature flag // Unsupported self serve type passed as feature flag
invalid = "invalid", invalid = "invalid",
// Add your self serve types here // Add your self serve types here
example = "example", // NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\
sqlx = "sqlx", example = "SelfServeExample",
graphapicompute = "graphapicompute", sqlx = "SqlX",
materializedviewsbuilder = "materializedviewsbuilder", graphapicompute = "GraphAPICompute",
materializedviewsbuilder = "MaterializedViewsBuilder",
} }
/** /**

View File

@@ -2,6 +2,7 @@
export enum Action { export enum Action {
CollapseTreeNode, CollapseTreeNode,
CreateCollection, CreateCollection,
CreateMaterializedView,
CreateDocument, CreateDocument,
CreateStoredProcedure, CreateStoredProcedure,
CreateTrigger, CreateTrigger,
@@ -119,6 +120,7 @@ export enum Action {
NotebooksGalleryPublishedCount, NotebooksGalleryPublishedCount,
SelfServe, SelfServe,
ExpandAddCollectionPaneAdvancedSection, ExpandAddCollectionPaneAdvancedSection,
ExpandAddMaterializedViewPaneAdvancedSection,
SchemaAnalyzerClickAnalyze, SchemaAnalyzerClickAnalyze,
SelfServeComponent, SelfServeComponent,
LaunchQuickstart, LaunchQuickstart,

View File

@@ -1,4 +1,4 @@
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract"; import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils"; import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -47,11 +47,21 @@ export interface VCoreMongoConnectionParams {
connectionString: string; connectionString: string;
} }
interface FabricContext { export interface FabricArtifactInfo {
connectionId: string; [CosmosDbArtifactType.MIRRORED_KEY]: {
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined; connectionId: string;
resourceTokenInfo: ResourceTokenInfo | undefined;
};
[CosmosDbArtifactType.MIRRORED_AAD]: undefined;
[CosmosDbArtifactType.NATIVE]: undefined;
}
export interface FabricContext<T extends CosmosDbArtifactType> {
fabricClientRpcVersion: string;
isReadOnly: boolean; isReadOnly: boolean;
isVisible: boolean; isVisible: boolean;
databaseName: string;
artifactType: CosmosDbArtifactType;
artifactInfo: FabricArtifactInfo[T];
} }
export type AdminFeedbackControlPolicy = export type AdminFeedbackControlPolicy =
@@ -70,7 +80,7 @@ export type AdminFeedbackPolicySettings = {
}; };
export interface UserContext { export interface UserContext {
readonly fabricContext?: FabricContext; readonly fabricContext?: FabricContext<CosmosDbArtifactType>;
readonly authType?: AuthType; readonly authType?: AuthType;
readonly masterKey?: string; readonly masterKey?: string;
readonly subscriptionId?: string; readonly subscriptionId?: string;

View File

@@ -89,3 +89,7 @@ export const getItemName = (): string => {
return "Items"; return "Items";
} }
}; };
export const isDataplaneRbacSupported = (apiType: string): boolean => {
return apiType === "SQL" || apiType === "Tables";
};

View File

@@ -1,6 +1,7 @@
export const autoPilotThroughput1K = 1000; export const autoPilotThroughput1K = 1000;
export const autoPilotIncrementStep = 1000; export const autoPilotIncrementStep = 1000;
export const autoPilotThroughput4K = 4000; export const autoPilotThroughput4K = 4000;
export const autoPilotThroughput10K = 10000;
export function isValidAutoPilotThroughput(maxThroughput: number): boolean { export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
if (!maxThroughput) { if (!maxThroughput) {

View File

@@ -1,11 +1,4 @@
import { import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import { configContext } from "ConfigContext";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
export function validateEndpoint( export function validateEndpoint(
@@ -73,9 +66,6 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
//"https://main2.documentdb.ext.azure.com": ["104.42.196.69"], //"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.cn": ["139.217.8.252"],
"https://main.documentdb.ext.azure.us": ["52.244.48.71"], "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[] } = { export const PortalBackendOutboundIPs: { [key: string]: string[] } = {
@@ -100,14 +90,6 @@ export const defaultAllowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Mooncake, 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> = [ export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
CassandraProxyEndpoints.Development, CassandraProxyEndpoints.Development,
CassandraProxyEndpoints.Mpac, CassandraProxyEndpoints.Mpac,
@@ -153,53 +135,3 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
]; ];
export const allowedNotebookServerUrls: 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);
}

View File

@@ -1,3 +1,4 @@
import { isFabric } from "Platform/Fabric/FabricUtil";
import { Platform, configContext } from "./../ConfigContext"; import { Platform, configContext } from "./../ConfigContext";
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => { export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
@@ -7,7 +8,7 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
if (currentWindow.parent === currentWindow) { if (currentWindow.parent === currentWindow) {
return undefined; 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 // in Fabric data explorer is inside an extension iframe, so we have two parent iframes
return currentWindow; return currentWindow;
} }

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs 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 { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; 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. */ /* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
export async function listCassandraKeyspaces( export async function listCassandraKeyspaces(

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs 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 { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; 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. */ /* Retrieves the metrics determined by the given filter for the given database account and collection. */
export async function listMetrics( export async function listMetrics(

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs 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 { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; 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. */ /* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
export async function listMetrics( export async function listMetrics(

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs 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 { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; 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 and region, split by partition. */ /* Retrieves the metrics determined by the given filter for the given collection and region, split by partition. */
export async function listMetrics( export async function listMetrics(

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs 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 { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; 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, collection and region. */ /* Retrieves the metrics determined by the given filter for the given database account, collection and region. */
export async function listMetrics( export async function listMetrics(

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs 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 { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; 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 database. */ /* Retrieves the metrics determined by the given filter for the given database account and database. */
export async function listMetrics( export async function listMetrics(

Some files were not shown because too many files have changed in this diff Show More