mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-24 19:31:36 +00:00
Compare commits
1 Commits
release/ma
...
before_opt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd905403b |
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|||||||
@@ -61,8 +61,6 @@
|
|||||||
|
|
||||||
@GalleryBackgroundColor: #fdfdfd;
|
@GalleryBackgroundColor: #fdfdfd;
|
||||||
|
|
||||||
@LinkColor: #2d6da4;
|
|
||||||
|
|
||||||
//Icons
|
//Icons
|
||||||
@InfoIconColor: #0072c6;
|
@InfoIconColor: #0072c6;
|
||||||
@WarningIconColor: #db7500;
|
@WarningIconColor: #db7500;
|
||||||
@@ -248,10 +246,6 @@
|
|||||||
outline: 1px dashed @FocusColor;
|
outline: 1px dashed @FocusColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focusedBorder() {
|
|
||||||
border: 1px dashed @FocusColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************************************************************************************
|
/************************************************************************************************
|
||||||
Common Toggle Switch
|
Common Toggle Switch
|
||||||
*************************************************************************************************/
|
*************************************************************************************************/
|
||||||
|
|||||||
@@ -530,13 +530,6 @@ 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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
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, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity } 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 { PriorityLevel } from "../Common/Constants";
|
import { BackendApi, 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 { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
import { 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";
|
||||||
@@ -20,7 +19,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 && isDataplaneRbacSupported(userContext.apiType);
|
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && userContext.apiType === "SQL";
|
||||||
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} `,
|
||||||
@@ -43,7 +42,7 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
|||||||
return decodeURIComponent(headers.authorization);
|
return decodeURIComponent(headers.authorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
switch (requestInfo.resourceType) {
|
switch (requestInfo.resourceType) {
|
||||||
case Cosmos.ResourceType.conflicts:
|
case Cosmos.ResourceType.conflicts:
|
||||||
case Cosmos.ResourceType.container:
|
case Cosmos.ResourceType.container:
|
||||||
@@ -55,13 +54,8 @@ 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 = (
|
const resourceTokens = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]
|
checkDatabaseResourceTokensValidity(userContext.fabricContext.databaseConnectionInfo.resourceTokensTimestamp);
|
||||||
).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:
|
||||||
@@ -72,9 +66,7 @@ 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 = (
|
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
|
||||||
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 **************
|
||||||
@@ -133,6 +125,10 @@ 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", {
|
||||||
@@ -155,6 +151,34 @@ 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,
|
||||||
@@ -179,10 +203,8 @@ 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 ||
|
||||||
|
|||||||
@@ -26,7 +26,3 @@ export function getWorkloadType(): WorkloadType {
|
|||||||
}
|
}
|
||||||
return workloadType;
|
return workloadType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMaterializedViewsEnabled(): boolean {
|
|
||||||
return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,8 +4,16 @@ 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 { deleteDocuments, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
import {
|
||||||
|
deleteDocument,
|
||||||
|
getEndpoint,
|
||||||
|
getFeatureEndpointOrDefault,
|
||||||
|
queryDocuments,
|
||||||
|
readDocument,
|
||||||
|
updateDocument,
|
||||||
|
} from "./MongoProxyClient";
|
||||||
|
|
||||||
const databaseId = "testDB";
|
const databaseId = "testDB";
|
||||||
|
|
||||||
@@ -188,8 +196,20 @@ 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("deleteDocuments", () => {
|
describe("deleteDocument", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -206,9 +226,9 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("builds the correct URL", () => {
|
it("builds the correct URL", () => {
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -218,9 +238,9 @@ describe("MongoProxyClient", () => {
|
|||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
globallyEnabledMongoAPIs: [],
|
||||||
});
|
});
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer/bulkdelete`,
|
`${configContext.MONGO_PROXY_ENDPOINT}/api/mongo/explorer`,
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -255,4 +275,33 @@ 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`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
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 } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyApi, MongoProxyEndpoints } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
import { sendMessage } from "./MessageHandler";
|
import { sendMessage } from "./MessageHandler";
|
||||||
|
|
||||||
@@ -60,6 +67,10 @@ 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 = {
|
||||||
@@ -78,7 +89,7 @@ export function queryDocuments(
|
|||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT) || "";
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ResourceList) || "";
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
@@ -116,11 +127,76 @@ 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("/");
|
||||||
@@ -141,7 +217,7 @@ export function readDocument(
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.ReadDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -161,12 +237,61 @@ 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 = {
|
||||||
@@ -183,7 +308,7 @@ export function createDocument(
|
|||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createDocument`, {
|
.fetch(`${endpoint}/createDocument`, {
|
||||||
@@ -203,12 +328,54 @@ 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("/");
|
||||||
@@ -229,7 +396,7 @@ export function updateDocument(
|
|||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.UpdateDocument);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(endpoint, {
|
.fetch(endpoint, {
|
||||||
@@ -250,6 +417,139 @@ 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,
|
||||||
@@ -275,7 +575,7 @@ export function deleteDocuments(
|
|||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.BulkDelete);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/bulkdelete`, {
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
@@ -299,6 +599,9 @@ 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];
|
||||||
|
|
||||||
@@ -319,7 +622,7 @@ export function createMongoCollectionWithProxy(
|
|||||||
isSharded: !!shardKey,
|
isSharded: !!shardKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getFeatureEndpointOrDefault(MongoProxyApi.CreateCollectionWithProxy);
|
||||||
|
|
||||||
return window
|
return window
|
||||||
.fetch(`${endpoint}/createCollection`, {
|
.fetch(`${endpoint}/createCollection`, {
|
||||||
@@ -339,6 +642,70 @@ 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";
|
||||||
|
|
||||||
@@ -352,6 +719,84 @@ 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);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
|
|
||||||
// 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 (isFabric()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
StyleConstants.AccentMediumHigh = StyleConstants.FabricAccentMediumHigh;
|
||||||
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
StyleConstants.AccentMedium = StyleConstants.FabricAccentMedium;
|
||||||
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
StyleConstants.AccentLight = StyleConstants.FabricAccentLight;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -25,7 +24,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
let collection: DataModels.Collection;
|
let collection: DataModels.Collection;
|
||||||
if (!isFabricNative() && userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
if (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,
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -13,7 +12,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 && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
await deleteCollectionWithARM(databaseId, collectionId);
|
await deleteCollectionWithARM(databaseId, collectionId);
|
||||||
} else {
|
} else {
|
||||||
await client().database(databaseId).container(collectionId).delete();
|
await client().database(databaseId).container(collectionId).delete();
|
||||||
|
|||||||
@@ -105,8 +105,6 @@ 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,
|
||||||
@@ -116,7 +114,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +125,6 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
|||||||
offerReplacePending: resource.offerReplacePending === "true",
|
offerReplacePending: resource.offerReplacePending === "true",
|
||||||
instantMaximumThroughput,
|
instantMaximumThroughput,
|
||||||
softAllowedMaximumThroughput,
|
softAllowedMaximumThroughput,
|
||||||
throughputBuckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ContainerResponse } from "@azure/cosmos";
|
import { ContainerResponse } from "@azure/cosmos";
|
||||||
import { Queries } from "Common/Constants";
|
import { Queries } from "Common/Constants";
|
||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
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 { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { 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";
|
||||||
@@ -17,13 +16,15 @@ 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 (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
|
if (
|
||||||
|
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 (
|
for (const collectionResourceId in userContext.fabricContext.databaseConnectionInfo.resourceTokens) {
|
||||||
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];
|
||||||
@@ -55,8 +56,7 @@ 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,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
|||||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TO DO: Remove when we get RP API Spec with materializedViews
|
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
||||||
/* 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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
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,9 +11,8 @@ 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 (isFabricMirroredKey() || isFabricNative()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
// For Fabric Mirroring, it is slow, because it requests the token and we don't need it.
|
// TODO This works, but is very slow, because it requests the token, so we skip for now
|
||||||
// For Fabric Native, it is not supported.
|
|
||||||
console.error("Skiping readDatabaseOffer for Fabric");
|
console.error("Skiping readDatabaseOffer for Fabric");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -24,8 +23,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
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 { FabricArtifactInfo, userContext } from "../../UserContext";
|
import { 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";
|
||||||
@@ -15,13 +14,8 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
|
|||||||
let databases: DataModels.Database[];
|
let databases: DataModels.Database[];
|
||||||
const clearMessage = logConsoleProgress(`Querying databases`);
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
|
||||||
if (
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.databaseConnectionInfo.resourceTokens) {
|
||||||
isFabricMirroredKey() &&
|
const tokensData = userContext.fabricContext.databaseConnectionInfo;
|
||||||
(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
|
||||||
|
|
||||||
@@ -52,28 +46,13 @@ 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 {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -37,8 +36,7 @@ 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 {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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, ThroughputBucket, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import {
|
import {
|
||||||
migrateCassandraKeyspaceToAutoscale,
|
migrateCassandraKeyspaceToAutoscale,
|
||||||
@@ -57,7 +56,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 && !isFabric()) {
|
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
|
||||||
if (params.collectionId) {
|
if (params.collectionId) {
|
||||||
updatedOffer = await updateCollectionOfferWithARM(params);
|
updatedOffer = await updateCollectionOfferWithARM(params);
|
||||||
} else if (userContext.apiType === "Tables") {
|
} else if (userContext.apiType === "Tables") {
|
||||||
@@ -360,13 +359,6 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
allowedGraphEndpoints,
|
allowedGraphEndpoints,
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
|
allowedMongoBackendEndpoints,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
@@ -49,8 +50,10 @@ 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[];
|
||||||
@@ -106,6 +109,7 @@ 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,
|
||||||
@@ -148,6 +152,15 @@ 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,
|
||||||
@@ -157,6 +170,10 @@ 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,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ 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[];
|
||||||
@@ -165,8 +164,6 @@ 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 {
|
||||||
@@ -226,17 +223,6 @@ 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";
|
||||||
@@ -289,12 +275,6 @@ 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 {
|
||||||
@@ -359,7 +339,9 @@ export interface CreateDatabaseParams {
|
|||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCollectionParamsBase {
|
export interface CreateCollectionParams {
|
||||||
|
createNewDatabase: boolean;
|
||||||
|
collectionId: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
@@ -373,16 +355,6 @@ export interface CreateCollectionParamsBase {
|
|||||||
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[];
|
||||||
}
|
}
|
||||||
@@ -425,7 +397,6 @@ export interface UpdateOfferParams {
|
|||||||
collectionId?: string;
|
collectionId?: string;
|
||||||
migrateToAutoPilot?: boolean;
|
migrateToAutoPilot?: boolean;
|
||||||
migrateToManual?: boolean;
|
migrateToManual?: boolean;
|
||||||
throughputBuckets?: ThroughputBucket[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
export enum FabricMessageTypes {
|
export enum FabricMessageTypes {
|
||||||
GetAuthorizationToken = "GetAuthorizationToken",
|
GetAuthorizationToken = "GetAuthorizationToken",
|
||||||
GetAllResourceTokens = "GetAllResourceTokens",
|
GetAllResourceTokens = "GetAllResourceTokens",
|
||||||
GetAccessToken = "GetAccessToken",
|
|
||||||
Ready = "Ready",
|
Ready = "Ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,47 @@
|
|||||||
import { AuthorizationToken } from "./FabricMessageTypes";
|
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
|
|
||||||
// This is the version of these messages
|
// This is the version of these messages
|
||||||
export const FABRIC_RPC_VERSION = "FabricMessageV3";
|
export const FABRIC_RPC_VERSION = "2";
|
||||||
|
|
||||||
// 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";
|
||||||
@@ -31,7 +69,7 @@ export type FabricMessageV2 =
|
|||||||
message: {
|
message: {
|
||||||
id: string;
|
id: string;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
data: ResourceTokenInfo | undefined;
|
data: FabricDatabaseConnectionInfo | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -41,81 +79,17 @@ export type FabricMessageV2 =
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FabricMessageV3 =
|
export type CosmosDBTokenResponse = {
|
||||||
| {
|
token: string;
|
||||||
type: "newContainer";
|
date: string;
|
||||||
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 enum CosmosDbArtifactType {
|
export type CosmosDBConnectionInfoResponse = {
|
||||||
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: Record<string, string> | undefined;
|
resourceTokens: { [resourceId: string]: string };
|
||||||
accessToken: string | undefined;
|
};
|
||||||
isReadOnly: boolean;
|
|
||||||
credentialType: "Key" | "OAuth2" | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResourceTokenInfo extends CosmosDBConnectionInfoResponse {
|
export interface FabricDatabaseConnectionInfo extends CosmosDBConnectionInfoResponse {
|
||||||
resourceTokensTimestamp: number;
|
resourceTokensTimestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,8 +143,6 @@ 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[];
|
||||||
@@ -408,6 +406,7 @@ 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;
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
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";
|
||||||
@@ -27,6 +19,7 @@ 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";
|
||||||
@@ -48,7 +41,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 (isFabric() && userContext.fabricContext?.isReadOnly) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +53,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
if (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteDatabaseIcon,
|
iconSrc: DeleteDatabaseIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
@@ -152,7 +145,7 @@ export const createCollectionContextMenuButton = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFabric() || (isFabric() && !userContext.fabricContext?.isReadOnly)) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||||
@@ -170,24 +163,6 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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";
|
||||||
@@ -249,42 +247,4 @@ 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ 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";
|
||||||
@@ -45,10 +41,6 @@ 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,
|
||||||
@@ -94,8 +86,6 @@ 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;
|
||||||
@@ -114,7 +104,6 @@ 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;
|
||||||
@@ -166,11 +155,9 @@ 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) {
|
||||||
@@ -184,16 +171,10 @@ 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 =
|
||||||
@@ -212,8 +193,6 @@ 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,
|
||||||
@@ -232,7 +211,6 @@ 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,
|
||||||
@@ -349,8 +327,7 @@ 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
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -362,8 +339,7 @@ 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
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -443,8 +419,6 @@ 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,
|
||||||
@@ -467,7 +441,6 @@ 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,
|
||||||
@@ -506,10 +479,6 @@ 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 });
|
||||||
@@ -780,13 +749,9 @@ 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,
|
||||||
@@ -874,10 +839,6 @@ 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 });
|
||||||
|
|
||||||
@@ -1068,24 +1029,6 @@ 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,
|
||||||
@@ -1266,24 +1209,12 @@ 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({
|
||||||
@@ -1340,20 +1271,6 @@ 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],
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
|
||||||
import { ThroughputBucket } from "Contracts/DataModels";
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
|
||||||
import { isDirty } from "../../SettingsUtils";
|
|
||||||
|
|
||||||
const MAX_BUCKET_SIZES = 5;
|
|
||||||
|
|
||||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export interface ThroughputBucketsComponentProps {
|
|
||||||
currentBuckets: ThroughputBucket[];
|
|
||||||
throughputBucketsBaseline: ThroughputBucket[];
|
|
||||||
onBucketsChange: (updatedBuckets: ThroughputBucket[]) => void;
|
|
||||||
onSaveableChange: (isSaveable: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = ({
|
|
||||||
currentBuckets,
|
|
||||||
throughputBucketsBaseline,
|
|
||||||
onBucketsChange,
|
|
||||||
onSaveableChange,
|
|
||||||
}) => {
|
|
||||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
|
||||||
if (!buckets || buckets.length === 0) {
|
|
||||||
return DEFAULT_BUCKETS;
|
|
||||||
}
|
|
||||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
|
||||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
maxThroughputPercentage: 100,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return adjustedDefaultBuckets.map(
|
|
||||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setThroughputBuckets(getThroughputBuckets(currentBuckets));
|
|
||||||
onSaveableChange(false);
|
|
||||||
}, [currentBuckets]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isChanged = isDirty(throughputBuckets, getThroughputBuckets(throughputBucketsBaseline));
|
|
||||||
onSaveableChange(isChanged);
|
|
||||||
}, [throughputBuckets]);
|
|
||||||
|
|
||||||
const handleBucketChange = (id: number, newValue: number) => {
|
|
||||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
|
||||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
|
||||||
);
|
|
||||||
setThroughputBuckets(updatedBuckets);
|
|
||||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
|
||||||
settingsChanged && onBucketsChange(updatedBuckets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggle = (id: number, checked: boolean) => {
|
|
||||||
handleBucketChange(id, checked ? 50 : 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
|
||||||
<Label>Throughput Buckets</Label>
|
|
||||||
<Stack>
|
|
||||||
{throughputBuckets?.map((bucket) => (
|
|
||||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
|
||||||
<Slider
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={bucket.maxThroughputPercentage}
|
|
||||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
|
||||||
showValue={false}
|
|
||||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
|
||||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
value={bucket.maxThroughputPercentage.toString()}
|
|
||||||
onChange={(event, newValue) => handleBucketChange(bucket.id, parseInt(newValue || "0", 10))}
|
|
||||||
type="number"
|
|
||||||
suffix="%"
|
|
||||||
styles={{
|
|
||||||
fieldGroup: { width: 80 },
|
|
||||||
}}
|
|
||||||
disabled={bucket.maxThroughputPercentage === 100}
|
|
||||||
/>
|
|
||||||
<Toggle
|
|
||||||
onText="Active"
|
|
||||||
offText="Inactive"
|
|
||||||
checked={bucket.maxThroughputPercentage !== 100}
|
|
||||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
|
||||||
styles={{ root: { marginBottom: 0 }, text: { fontSize: 12 } }}
|
|
||||||
></Toggle>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -11,8 +11,7 @@ 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";
|
||||||
@@ -56,8 +55,6 @@ export enum SettingsV2TabTypes {
|
|||||||
PartitionKeyTab,
|
PartitionKeyTab,
|
||||||
ComputedPropertiesTab,
|
ComputedPropertiesTab,
|
||||||
ContainerVectorPolicyTab,
|
ContainerVectorPolicyTab,
|
||||||
ThroughputBucketsTab,
|
|
||||||
MaterializedViewTab,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerPolicyTabTypes {
|
export enum ContainerPolicyTabTypes {
|
||||||
@@ -170,10 +167,6 @@ 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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,15 +48,6 @@ 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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ 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",
|
||||||
@@ -141,8 +139,6 @@ 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",
|
||||||
@@ -262,8 +258,6 @@ 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",
|
||||||
@@ -342,101 +336,6 @@ 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>
|
||||||
|
|||||||
@@ -35,20 +35,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
setIsThroughputCapExceeded,
|
setIsThroughputCapExceeded,
|
||||||
onCostAcknowledgeChange,
|
onCostAcknowledgeChange,
|
||||||
}: ThroughputInputProps) => {
|
}: ThroughputInputProps) => {
|
||||||
let defaultThroughput: number;
|
const defaultThroughput: number =
|
||||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
|
||||||
|
|
||||||
if (
|
|
||||||
isFreeTier ||
|
isFreeTier ||
|
||||||
isQuickstart ||
|
isQuickstart ||
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(getWorkloadType())
|
||||||
) {
|
? AutoPilotUtils.autoPilotThroughput1K
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
: AutoPilotUtils.autoPilotThroughput4K;
|
||||||
} 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);
|
||||||
|
|||||||
@@ -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 { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
import { scheduleRefreshDatabaseResourceToken } 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 { ReactTabKind, useTabs } from "../hooks/useTabs";
|
import { 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/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/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,10 +187,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +347,8 @@ export default class Explorer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshResourcesClick = async (): Promise<void> => {
|
public onRefreshResourcesClick = async (): Promise<void> => {
|
||||||
if (isFabricMirroredKey()) {
|
if (configContext.platform === Platform.Fabric) {
|
||||||
scheduleRefreshFabricToken(true).then(() => this.refreshAllDatabases());
|
scheduleRefreshDatabaseResourceToken(true).then(() => this.refreshAllDatabases());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1131,7 +1127,7 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshSampleData();
|
await this.refreshSampleData();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async configureCopilot(): Promise<void> {
|
public async configureCopilot(): Promise<void> {
|
||||||
@@ -1156,27 +1152,26 @@ export default class Explorer {
|
|||||||
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
.setCopilotSampleDBEnabled(copilotEnabled && copilotUserDBEnabled && copilotSampleDBEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshSampleData(): void {
|
public async refreshSampleData(): Promise<void> {
|
||||||
if (!userContext.sampleDataConnectionInfo) {
|
try {
|
||||||
|
if (!userContext.sampleDataConnectionInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collection: DataModels.Collection = await readSampleCollection();
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const databaseId = userContext.sampleDataConnectionInfo?.databaseId;
|
||||||
|
if (!databaseId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
|
||||||
|
useDatabases.setState({ sampleDataResourceTokenCollection });
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(getErrorMessage(error), "Explorer");
|
||||||
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");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
.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;
|
||||||
@@ -59,10 +63,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customTrashIcon {
|
|
||||||
padding-top: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightPaneTrashIconImg {
|
.rightPaneTrashIconImg {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,11 +142,10 @@ 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}
|
||||||
required
|
aria-required="true"
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
aria-label={`Enter value for propery ${index + 1}`}
|
aria-label={`Enter value for propery ${index + 1}`}
|
||||||
@@ -154,11 +153,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">* </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"
|
||||||
@@ -170,8 +169,6 @@ 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 }}
|
||||||
@@ -184,7 +181,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
|||||||
</div>
|
</div>
|
||||||
<div className="actionCol">
|
<div className="actionCol">
|
||||||
<div
|
<div
|
||||||
className={`rightPaneTrashIcon rightPaneBtns ${index === 0 && "customTrashIcon"}`}
|
className="rightPaneTrashIcon rightPaneBtns"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={`Delete ${data.key}`}
|
aria-label={`Delete ${data.key}`}
|
||||||
|
|||||||
@@ -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,18 +93,19 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootStyle = isFabric()
|
const rootStyle =
|
||||||
? {
|
configContext.platform === Platform.Fabric
|
||||||
root: {
|
? {
|
||||||
backgroundColor: "transparent",
|
root: {
|
||||||
padding: "2px 8px 0px 8px",
|
backgroundColor: "transparent",
|
||||||
},
|
padding: "2px 8px 0px 8px",
|
||||||
}
|
},
|
||||||
: {
|
}
|
||||||
root: {
|
: {
|
||||||
backgroundColor: backgroundColor,
|
root: {
|
||||||
},
|
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);
|
||||||
|
|||||||
@@ -37,25 +37,21 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
|||||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
|
it("Button should not be visible for Tables API", () => {
|
||||||
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
|
updateUserContext({
|
||||||
// unsupported in jest and needs to be tested with react-hooks-testing-library.
|
databaseAccount: {
|
||||||
//
|
properties: {
|
||||||
// it("Button should not be visible for Tables API", () => {
|
capabilities: [{ name: "EnableTable" }],
|
||||||
// updateUserContext({
|
},
|
||||||
// databaseAccount: {
|
} as DatabaseAccount,
|
||||||
// properties: {
|
});
|
||||||
// capabilities: [{ name: "EnableTable" }],
|
|
||||||
// },
|
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||||
// } as DatabaseAccount,
|
const enableAzureSynapseLinkBtn = buttons.find(
|
||||||
// });
|
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
|
||||||
//
|
);
|
||||||
// const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||||
// 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({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -62,7 +61,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (userContext.apiType === "SQL") {
|
||||||
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);
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
&:active {
|
&:active {
|
||||||
background-color:@NotificationHigh;
|
background-color:@NotificationHigh;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
|
||||||
.focusedBorder();
|
|
||||||
}
|
|
||||||
|
|
||||||
.statusBar {
|
.statusBar {
|
||||||
.dataTypeIcons {
|
.dataTypeIcons {
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ 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,
|
||||||
@@ -97,9 +101,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
id="notificationConsoleHeader"
|
||||||
role="button"
|
ref={this.setElememntRef}
|
||||||
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}
|
||||||
@@ -107,15 +109,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>
|
||||||
@@ -127,10 +129,17 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="expandCollapseButton" data-test="NotificationConsole/ExpandCollapseButton">
|
<div
|
||||||
|
className="expandCollapseButton"
|
||||||
|
data-test="NotificationConsole/ExpandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||||
|
aria-expanded={!this.props.isConsoleExpanded}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||||
alt={this.props.isConsoleExpanded ? "Collapse icon" : "Expand icon"}
|
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,6 +259,9 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ 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
|
||||||
@@ -24,7 +21,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
|
||||||
@@ -37,7 +34,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -50,7 +47,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -74,11 +71,15 @@ 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="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,13 +176,10 @@ 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
|
||||||
@@ -194,7 +192,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
|
||||||
@@ -207,7 +205,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Error items"
|
alt="error items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -220,7 +218,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
className="notificationConsoleHeaderIconWithData"
|
className="notificationConsoleHeaderIconWithData"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Info items"
|
alt="info items"
|
||||||
src={{}}
|
src={{}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -246,11 +244,15 @@ 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="Expand icon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -127,7 +128,9 @@ 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 = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
const disallowedLocationsUri: string = useNewPortalBackendEndpoint(Constants.BackendApi.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, {
|
||||||
|
|||||||
@@ -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 (
|
||||||
isFabricMirrored() &&
|
configContext.platform === Platform.Fabric &&
|
||||||
!(
|
!(
|
||||||
// whitelist the tab kinds that are allowed to be opened in Fabric mirrored
|
// whitelist the tab kinds that are allowed to be opened in Fabric
|
||||||
(
|
(
|
||||||
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
|
||||||
action.tabKind === ActionContracts.TabKind.SQLQuery
|
action.tabKind === ActionContracts.TabKind.SQLQuery
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -21,28 +21,13 @@ 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 { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
|
||||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
|
||||||
import {
|
import {
|
||||||
AllPropertiesIndexed,
|
FullTextPoliciesComponent,
|
||||||
AnalyticalStorageContent,
|
getFullTextLanguageOptions,
|
||||||
ContainerVectorPolicyTooltipContent,
|
} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||||
FullTextPolicyDefault,
|
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||||
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";
|
||||||
@@ -56,15 +41,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;
|
||||||
@@ -72,6 +57,40 @@ 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: [],
|
||||||
};
|
};
|
||||||
@@ -124,7 +143,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: getPartitionKey(props.isQuickstart),
|
partitionKey: this.getPartitionKey(),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
@@ -140,7 +159,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
vectorEmbeddingPolicy: [],
|
vectorEmbeddingPolicy: [],
|
||||||
vectorIndexingPolicy: [],
|
vectorIndexingPolicy: [],
|
||||||
vectorPolicyValidated: true,
|
vectorPolicyValidated: true,
|
||||||
fullTextPolicy: FullTextPolicyDefault,
|
fullTextPolicy: { defaultLanguage: getFullTextLanguageOptions()[0].key as never, fullTextPaths: [] },
|
||||||
fullTextIndexes: [],
|
fullTextIndexes: [],
|
||||||
fullTextPolicyValidated: true,
|
fullTextPolicyValidated: true,
|
||||||
};
|
};
|
||||||
@@ -154,7 +173,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) {
|
||||||
scrollToSection("panelContainer");
|
this.scrollToSection("panelContainer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +190,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!this.state.errorMessage && isFreeTierAccount() && (
|
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||||
<PanelInfoErrorComponent
|
<PanelInfoErrorComponent
|
||||||
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
message={getUpsellMessage(userContext.portalEnv, true, isFirstResourceCreated, true)}
|
||||||
messageType="info"
|
messageType="info"
|
||||||
@@ -265,152 +284,150 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="panelMainContent">
|
<div className="panelMainContent">
|
||||||
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
<Stack hidden={userContext.apiType === "Tables"}>
|
||||||
<Stack hidden={userContext.apiType === "Tables"}>
|
<Stack horizontal>
|
||||||
<Stack horizontal>
|
<span className="mandatoryStar">* </span>
|
||||||
<span className="mandatoryStar">* </span>
|
<Text className="panelTextBold" variant="small">
|
||||||
<Text className="panelTextBold" variant="small">
|
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
||||||
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
</Text>
|
||||||
</Text>
|
<TooltipHost
|
||||||
<TooltipHost
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
true,
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
true,
|
true,
|
||||||
).toLocaleLowerCase()}.`}
|
).toLocaleLowerCase()}.`}
|
||||||
>
|
/>
|
||||||
<Icon
|
</TooltipHost>
|
||||||
iconName="Info"
|
</Stack>
|
||||||
className="panelInfoIcon"
|
|
||||||
tabIndex={0}
|
|
||||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
|
||||||
true,
|
|
||||||
).toLocaleLowerCase()}.`}
|
|
||||||
/>
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{configContext.platform !== Platform.Fabric && (
|
{configContext.platform !== Platform.Fabric && (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<div role="radiogroup">
|
<div role="radiogroup">
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={this.state.createNewDatabase}
|
|
||||||
aria-label="Create new database"
|
|
||||||
aria-checked={this.state.createNewDatabase}
|
|
||||||
name="databaseType"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="databaseCreateNew"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Create new</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={!this.state.createNewDatabase}
|
|
||||||
aria-label="Use existing database"
|
|
||||||
aria-checked={!this.state.createNewDatabase}
|
|
||||||
name="databaseType"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Use existing</span>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.createNewDatabase && (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<input
|
<input
|
||||||
name="newDatabaseId"
|
className="panelRadioBtn"
|
||||||
id="newDatabaseId"
|
checked={this.state.createNewDatabase}
|
||||||
aria-required
|
aria-label="Create new database"
|
||||||
required
|
aria-checked={this.state.createNewDatabase}
|
||||||
type="text"
|
name="databaseType"
|
||||||
autoComplete="off"
|
type="radio"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
role="radio"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
id="databaseCreateNew"
|
||||||
placeholder="Type a new database id"
|
|
||||||
size={40}
|
|
||||||
className="panelTextField"
|
|
||||||
aria-label="New database id, Type a new database id"
|
|
||||||
autoFocus
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
value={this.state.newDatabaseId}
|
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
this.setState({ newDatabaseId: event.target.value })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Create new</span>
|
||||||
|
|
||||||
{!isServerlessAccount() && (
|
<input
|
||||||
<Stack horizontal>
|
className="panelRadioBtn"
|
||||||
<Checkbox
|
checked={!this.state.createNewDatabase}
|
||||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
aria-label="Use existing database"
|
||||||
checked={this.state.isSharedThroughputChecked}
|
aria-checked={!this.state.createNewDatabase}
|
||||||
styles={{
|
name="databaseType"
|
||||||
text: { fontSize: 12 },
|
type="radio"
|
||||||
checkbox: { width: 12, height: 12 },
|
role="radio"
|
||||||
label: { padding: 0, alignItems: "center" },
|
tabIndex={0}
|
||||||
}}
|
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
/>
|
||||||
this.setState({ isSharedThroughputChecked: isChecked })
|
<span className="panelRadioBtnLabel">Use existing</span>
|
||||||
}
|
</div>
|
||||||
/>
|
</Stack>
|
||||||
<TooltipHost
|
)}
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
{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,
|
||||||
|
).toLocaleLowerCase()} within the database.`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
iconName="Info"
|
||||||
|
className="panelInfoIcon"
|
||||||
|
tabIndex={0}
|
||||||
|
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||||
true,
|
true,
|
||||||
).toLocaleLowerCase()} within the database.`}
|
).toLocaleLowerCase()} within the database.`}
|
||||||
>
|
/>
|
||||||
<Icon
|
</TooltipHost>
|
||||||
iconName="Info"
|
</Stack>
|
||||||
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={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={true}
|
isDatabase={true}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={this.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>
|
||||||
@@ -559,14 +576,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{getPartitionKeyName()}
|
{this.getPartitionKeyName()}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={this.getPartitionKeyTooltipText()}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
iconName="Info"
|
iconName="Info"
|
||||||
className="panelInfoIcon"
|
className="panelInfoIcon"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ariaLabel={getPartitionKeyTooltipText()}
|
ariaLabel={this.getPartitionKeyTooltipText()}
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -580,8 +600,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
placeholder={getPartitionKeyPlaceHolder()}
|
placeholder={this.getPartitionKeyPlaceHolder()}
|
||||||
aria-label={getPartitionKeyName()}
|
aria-label={this.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}
|
||||||
@@ -619,8 +639,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={getPartitionKeyPlaceHolder(index)}
|
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
||||||
aria-label={getPartitionKeyName()}
|
aria-label={this.getPartitionKeyName()}
|
||||||
pattern={".*"}
|
pattern={".*"}
|
||||||
title={""}
|
title={""}
|
||||||
value={subPartitionKey}
|
value={subPartitionKey}
|
||||||
@@ -646,7 +666,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{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 } }}
|
||||||
@@ -711,10 +731,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
isSharded={this.state.isSharded}
|
isSharded={this.state.isSharded}
|
||||||
isFreeTier={isFreeTierAccount()}
|
isFreeTier={this.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)}
|
||||||
@@ -727,9 +747,29 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Stack>
|
<Stack>
|
||||||
{UniqueKeysHeader()}
|
<Stack horizontal>
|
||||||
|
<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>
|
||||||
@@ -777,10 +817,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
{this.shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{AnalyticalStorageContent()}
|
{this.getAnalyticalStorageContent()}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
@@ -788,7 +828,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={this.state.enableAnalyticalStore}
|
checked={this.state.enableAnalyticalStore}
|
||||||
disabled={!isSynapseLinkEnabled()}
|
disabled={!this.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"
|
||||||
@@ -803,7 +843,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
<input
|
<input
|
||||||
className="panelRadioBtn"
|
className="panelRadioBtn"
|
||||||
checked={!this.state.enableAnalyticalStore}
|
checked={!this.state.enableAnalyticalStore}
|
||||||
disabled={!isSynapseLinkEnabled()}
|
disabled={!this.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"
|
||||||
@@ -817,7 +857,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!isSynapseLinkEnabled() && (
|
{!this.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{" "}
|
||||||
@@ -847,9 +887,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
title="Container Vector Policy"
|
title="Container Vector Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
@@ -875,7 +915,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={() => {
|
||||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
this.scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
//TODO: uncomment when learn more text becomes available
|
//TODO: uncomment when learn more text becomes available
|
||||||
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
// tooltipContent={this.getContainerFullTextPolicyTooltipContent()}
|
||||||
@@ -897,13 +937,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
{userContext.apiType !== "Tables" && (
|
||||||
<CollapsibleSectionComponent
|
<CollapsibleSectionComponent
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||||
scrollToSection("collapsibleAdvancedSectionContent");
|
this.scrollToSection("collapsibleAdvancedSectionContent");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||||
@@ -1013,6 +1053,31 @@ 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({
|
||||||
@@ -1100,12 +1165,48 @@ 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. It’s 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 &&
|
||||||
@@ -1117,6 +1218,34 @@ 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 (
|
||||||
@@ -1131,7 +1260,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isFabricNative() || isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1147,7 +1276,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
private shouldShowIndexingOptionsForFreeTierAccount(): boolean {
|
||||||
if (!isFreeTierAccount()) {
|
if (!this.isFreeTierAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,6 +1285,39 @@ 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());
|
||||||
}
|
}
|
||||||
@@ -1236,11 +1398,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticalStorageTtl(): number {
|
private getAnalyticalStorageTtl(): number {
|
||||||
if (!isSynapseLinkEnabled()) {
|
if (!this.isSynapseLinkEnabled()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldShowAnalyticalStoreOptions()) {
|
if (!this.shouldShowAnalyticalStoreOptions()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1254,6 +1416,10 @@ 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()
|
||||||
@@ -1288,7 +1454,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
partitionKeyString = "/'$pk'";
|
partitionKeyString = "/'$pk'";
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(this.state.uniqueKeys);
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
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. It’s 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: [],
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
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">* </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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
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">* </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">* </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">* </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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 };
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
// 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>
|
|
||||||
`;
|
|
||||||
@@ -94,7 +94,6 @@
|
|||||||
padding-left: @MediumSpace;
|
padding-left: @MediumSpace;
|
||||||
|
|
||||||
.paneErrorLink {
|
.paneErrorLink {
|
||||||
color: @LinkColor;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ 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";
|
||||||
@@ -184,7 +183,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 =
|
||||||
isDataplaneRbacSupported(userContext.apiType) &&
|
userContext.apiType === "SQL" &&
|
||||||
userContext.authType === AuthType.AAD &&
|
userContext.authType === AuthType.AAD &&
|
||||||
configContext.platform !== Platform.Fabric &&
|
configContext.platform !== Platform.Fabric &&
|
||||||
!isEmulator;
|
!isEmulator;
|
||||||
|
|||||||
@@ -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/AddCollectionPanelUtility";
|
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
||||||
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";
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import { FeedbackLabels, HttpStatusCodes, NormalizedEventKey } from "Common/Constants";
|
import { 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,7 +393,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
disabled={isGeneratingQuery}
|
disabled={isGeneratingQuery}
|
||||||
autoComplete="off"
|
autoComplete="list"
|
||||||
|
aria-expanded={showSamplePrompts}
|
||||||
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
placeholder="Ask a question in natural language and we’ll generate the query for you."
|
||||||
aria-labelledby="copilot-textfield-label"
|
aria-labelledby="copilot-textfield-label"
|
||||||
onRenderSuffix={() => {
|
onRenderSuffix={() => {
|
||||||
@@ -579,7 +580,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 }}>{FeedbackLabels.provideFeedback}</Text>
|
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
|
||||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||||
<Callout
|
<Callout
|
||||||
role="status"
|
role="status"
|
||||||
@@ -629,9 +630,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
id="likeBtn"
|
id="likeBtn"
|
||||||
style={{ marginLeft: 10 }}
|
style={{ marginLeft: 10 }}
|
||||||
aria-label={FeedbackLabels.provideFeedback}
|
aria-label="Like"
|
||||||
role="button"
|
role="toggle"
|
||||||
title="Like"
|
|
||||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCallout(!likeQuery);
|
setShowCallout(!likeQuery);
|
||||||
@@ -649,9 +649,8 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
style={{ margin: "0 4px" }}
|
style={{ margin: "0 4px" }}
|
||||||
role="button"
|
role="toggle"
|
||||||
aria-label={FeedbackLabels.provideFeedback}
|
aria-label="Dislike"
|
||||||
title="Dislike"
|
|
||||||
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let toggleStatusValue = "Unpressed";
|
let toggleStatusValue = "Unpressed";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FeedOptions } from "@azure/cosmos";
|
import { FeedOptions } from "@azure/cosmos";
|
||||||
import {
|
import {
|
||||||
Areas,
|
Areas,
|
||||||
|
BackendApi,
|
||||||
ConnectionStatusType,
|
ConnectionStatusType,
|
||||||
ContainerStatusType,
|
ContainerStatusType,
|
||||||
HttpStatusCodes,
|
HttpStatusCodes,
|
||||||
@@ -31,6 +32,7 @@ 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";
|
||||||
@@ -80,7 +82,9 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getCopilotEnabled = async (): Promise<boolean> => {
|
export const getCopilotEnabled = async (): Promise<boolean> => {
|
||||||
const backendEndpoint: string = configContext.PORTAL_BACKEND_ENDPOINT;
|
const backendEndpoint: string = useNewPortalBackendEndpoint(BackendApi.PortalSettings)
|
||||||
|
? 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();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
makeStyles,
|
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuButtonProps,
|
MenuButtonProps,
|
||||||
@@ -8,26 +7,20 @@ 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 { MaterializedViewsLabels } from "Common/Constants";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
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";
|
||||||
@@ -130,7 +123,7 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
|
|
||||||
const actions = useMemo<GlobalCommand[]>(() => {
|
const actions = useMemo<GlobalCommand[]>(() => {
|
||||||
if (
|
if (
|
||||||
(isFabric() && userContext.fabricContext?.isReadOnly) ||
|
configContext.platform === Platform.Fabric ||
|
||||||
userContext.apiType === "Postgres" ||
|
userContext.apiType === "Postgres" ||
|
||||||
userContext.apiType === "VCoreMongo"
|
userContext.apiType === "VCoreMongo"
|
||||||
) {
|
) {
|
||||||
@@ -144,15 +137,12 @@ const GlobalCommands: React.FC<GlobalCommandsProps> = ({ explorer }) => {
|
|||||||
id: "new_collection",
|
id: "new_collection",
|
||||||
label: `New ${getCollectionName()}`,
|
label: `New ${getCollectionName()}`,
|
||||||
icon: <Add16Regular />,
|
icon: <Add16Regular />,
|
||||||
onClick: () => {
|
onClick: () => explorer.onNewCollectionClicked(),
|
||||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
|
||||||
explorer.onNewCollectionClicked({ databaseId });
|
|
||||||
},
|
|
||||||
keyboardAction: KeyboardAction.NEW_COLLECTION,
|
keyboardAction: KeyboardAction.NEW_COLLECTION,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (configContext.platform !== Platform.Fabric && userContext.apiType !== "Tables") {
|
if (userContext.apiType !== "Tables") {
|
||||||
actions.push({
|
actions.push({
|
||||||
id: "new_database",
|
id: "new_database",
|
||||||
label: `New ${getDatabaseName()}`,
|
label: `New ${getDatabaseName()}`,
|
||||||
@@ -168,25 +158,6 @@ 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]);
|
||||||
|
|
||||||
@@ -317,7 +288,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
|||||||
}, [setLoading]);
|
}, [setLoading]);
|
||||||
|
|
||||||
const hasGlobalCommands = !(
|
const hasGlobalCommands = !(
|
||||||
isFabricMirrored() ||
|
configContext.platform === Platform.Fabric ||
|
||||||
userContext.apiType === "Postgres" ||
|
userContext.apiType === "Postgres" ||
|
||||||
userContext.apiType === "VCoreMongo"
|
userContext.apiType === "VCoreMongo"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
/**
|
|
||||||
* Accordion top class
|
|
||||||
*/
|
|
||||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
|
||||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
|
||||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
|
||||||
import * as React from "react";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
|
|
||||||
export interface SplashScreenProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
homeContainer: {
|
|
||||||
width: "100%",
|
|
||||||
alignContent: "center",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: "center",
|
|
||||||
fontSize: "20px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
buttonsContainer: {
|
|
||||||
width: "584px",
|
|
||||||
margin: "auto",
|
|
||||||
display: "grid",
|
|
||||||
padding: "16px",
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
gap: "10px",
|
|
||||||
gridAutoRows: "minmax(184px, auto)",
|
|
||||||
},
|
|
||||||
one: {
|
|
||||||
gridColumn: "1 / 3",
|
|
||||||
gridRow: "1 / 3",
|
|
||||||
"& svg": {
|
|
||||||
width: "48px",
|
|
||||||
height: "48px",
|
|
||||||
margin: "auto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
two: {
|
|
||||||
gridColumn: "3",
|
|
||||||
gridRow: "1",
|
|
||||||
"& img": {
|
|
||||||
width: "32px",
|
|
||||||
height: "32px",
|
|
||||||
margin: "auto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
three: {
|
|
||||||
gridColumn: "3",
|
|
||||||
gridRow: "2",
|
|
||||||
"& svg": {
|
|
||||||
width: "32px",
|
|
||||||
height: "32px",
|
|
||||||
margin: "auto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
border: "1px solid #e0e0e0",
|
|
||||||
cursor: "pointer",
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: tokens.colorNeutralBackground1Hover,
|
|
||||||
"border-color": tokens.colorNeutralStroke1Hover,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buttonUpperPart: {
|
|
||||||
textAlign: "center",
|
|
||||||
flexGrow: 1,
|
|
||||||
display: "flex",
|
|
||||||
backgroundColor: "#e3f7ef",
|
|
||||||
},
|
|
||||||
buttonLowerPart: {
|
|
||||||
borderTop: "1px solid #e0e0e0",
|
|
||||||
height: "76px",
|
|
||||||
padding: "8px",
|
|
||||||
"> div:nth-child(1)": {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
textAlign: "center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface FabricHomeScreenButtonProps {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: JSX.Element;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className: string }> = ({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
// TODO Make this a11y copmliant: aria-label for icon
|
|
||||||
return (
|
|
||||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
|
||||||
<div className={styles.buttonUpperPart}>{icon}</div>
|
|
||||||
<div className={styles.buttonLowerPart}>
|
|
||||||
<div>{title}</div>
|
|
||||||
<div>{description}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScreenProps) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const getSplashScreenButtons = (): JSX.Element => {
|
|
||||||
const buttons: FabricHomeScreenButtonProps[] = [
|
|
||||||
{
|
|
||||||
title: "New container",
|
|
||||||
description: "Create a destination container to store your data",
|
|
||||||
icon: <DocumentAddRegular />,
|
|
||||||
onClick: () => {
|
|
||||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
|
||||||
props.explorer.onNewCollectionClicked({ databaseId });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Sample data",
|
|
||||||
description: "Automatically load sample data in your database",
|
|
||||||
icon: <img src={CosmosDbBlackIcon} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "App development",
|
|
||||||
description: "Start here to use an SDK to build your apps",
|
|
||||||
icon: <LinkMultipleRegular />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.buttonsContainer}>
|
|
||||||
<FabricHomeScreenButton className={styles.one} {...buttons[0]} />
|
|
||||||
<FabricHomeScreenButton className={styles.two} {...buttons[1]} />
|
|
||||||
<FabricHomeScreenButton className={styles.three} {...buttons[2]} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const title = "Build your database";
|
|
||||||
return (
|
|
||||||
<div className={styles.homeContainer}>
|
|
||||||
<div className={styles.title} role="heading" aria-label={title}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
{getSplashScreenButtons()}
|
|
||||||
<div className={styles.footer}>
|
|
||||||
Need help?{" "}
|
|
||||||
<Link href="https://cosmos.azure.com/docs" target="_blank">
|
|
||||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,7 @@ import * as ko from "knockout";
|
|||||||
import Q from "q";
|
import 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 } from "../../Common/Constants";
|
import { CassandraProxyAPIs, CassandraProxyEndpoints } 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,6 +264,9 @@ 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 {
|
||||||
@@ -306,6 +309,55 @@ 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[],
|
||||||
@@ -419,6 +471,10 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -459,7 +515,52 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -501,7 +602,52 @@ 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 =
|
||||||
@@ -531,6 +677,38 @@ 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;
|
||||||
@@ -569,4 +747,23 @@ 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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";
|
||||||
@@ -342,15 +341,10 @@ describe("Documents tab (noSql API)", () => {
|
|||||||
updateConfigContext({ platform: Platform.Fabric });
|
updateConfigContext({ platform: Platform.Fabric });
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
fabricContext: {
|
fabricContext: {
|
||||||
databaseName: "database",
|
connectionId: "test",
|
||||||
artifactInfo: {
|
databaseConnectionInfo: undefined,
|
||||||
connectionId: "test",
|
|
||||||
resourceTokenInfo: undefined,
|
|
||||||
},
|
|
||||||
artifactType: CosmosDbArtifactType.MIRRORED_KEY,
|
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
fabricClientRpcVersion: "rpcVersion",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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";
|
||||||
@@ -42,7 +43,6 @@ 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 (isFabric() && userContext.fabricContext?.isReadOnly) {
|
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
|
||||||
// All the following buttons require write access
|
// All the following buttons require write access
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1150,16 +1150,27 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
deletePromise = MongoProxyClient.deleteDocuments(
|
if (isMongoBulkDeleteDisabled) {
|
||||||
_collection.databaseId,
|
// TODO: Once new mongo proxy is available for all users, remove the call for MongoProxyClient.deleteDocument().
|
||||||
_collection as ViewModels.Collection,
|
// MongoProxyClient.deleteDocuments() should be called for all users.
|
||||||
toDeleteDocumentIds,
|
deletePromise = MongoProxyClient.deleteDocument(
|
||||||
).then(({ deletedCount, isAcknowledged }) => {
|
_collection.databaseId,
|
||||||
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
|
_collection as ViewModels.Collection,
|
||||||
return toDeleteDocumentIds;
|
toDeleteDocumentIds[0],
|
||||||
}
|
).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
|
||||||
@@ -2043,8 +2054,11 @@ 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 isBulkDeleteDisabled = partitionKey.systemKey && !isPreferredApiMongoDB;
|
const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint(Constants.MongoProxyApi.BulkDelete);
|
||||||
|
const isBulkDeleteDisabled =
|
||||||
|
(partitionKey.systemKey && !isPreferredApiMongoDB) || (isPreferredApiMongoDB && isMongoBulkDeleteDisabled);
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
|
|
||||||
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
const getFilterChoices = (): InputDatalistDropdownOptionSection[] => {
|
||||||
@@ -2136,7 +2150,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
selectedColumnIds={selectedColumnIds}
|
selectedColumnIds={selectedColumnIds}
|
||||||
columnDefinitions={columnDefinitions}
|
columnDefinitions={columnDefinitions}
|
||||||
isRowSelectionDisabled={
|
isRowSelectionDisabled={
|
||||||
isBulkDeleteDisabled || (isFabric() && userContext.fabricContext?.isReadOnly)
|
isBulkDeleteDisabled ||
|
||||||
|
(configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly)
|
||||||
}
|
}
|
||||||
onColumnSelectionChange={onColumnSelectionChange}
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
defaultColumnSelection={getInitialColumnSelection()}
|
defaultColumnSelection={getInitialColumnSelection()}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
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";
|
||||||
@@ -48,13 +50,15 @@ 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(),
|
url: getMongoShellUrl(this._useMongoProxyEndpoint),
|
||||||
};
|
};
|
||||||
|
|
||||||
props.onMongoShellTabAccessor({
|
props.onMongoShellTabAccessor({
|
||||||
@@ -109,9 +113,17 @@ 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 = configContext.MONGO_PROXY_ENDPOINT;
|
const apiEndpoint = this._useMongoProxyEndpoint
|
||||||
|
? configContext.MONGO_PROXY_ENDPOINT
|
||||||
|
: configContext.BACKEND_ENDPOINT;
|
||||||
const encryptedAuthToken: string = userContext.accessToken;
|
const encryptedAuthToken: string = userContext.accessToken;
|
||||||
|
|
||||||
shellIframe.contentWindow.postMessage(
|
shellIframe.contentWindow.postMessage(
|
||||||
@@ -120,7 +132,7 @@ export default class MongoShellTabComponent extends Component<
|
|||||||
data: {
|
data: {
|
||||||
resourceId: resourceId,
|
resourceId: resourceId,
|
||||||
accountName: accountName,
|
accountName: accountName,
|
||||||
mongoEndpoint: documentEndpoint,
|
mongoEndpoint: this._useMongoProxyEndpoint ? documentEndpoint : mongoEndpoint,
|
||||||
authorization: authorization,
|
authorization: authorization,
|
||||||
databaseId: databaseId,
|
databaseId: databaseId,
|
||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ 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 = "";
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ describe("getMongoShellUrl", () => {
|
|||||||
resetConfigContext();
|
resetConfigContext();
|
||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
|
BACKEND_ENDPOINT: mongoBackendEndpoint,
|
||||||
platform: Platform.Hosted,
|
platform: Platform.Hosted,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,7 +37,12 @@ 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 /index.html by default", () => {
|
it("should return /indexv2.html by default", () => {
|
||||||
expect(getMongoShellUrl().toString()).toContain(`/index.html?${queryString}`);
|
expect(getMongoShellUrl().toString()).toContain(`/indexv2.html?${queryString}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return /index.html when useMongoProxyEndpoint is true", () => {
|
||||||
|
const useMongoProxyEndpoint: boolean = true;
|
||||||
|
expect(getMongoShellUrl(useMongoProxyEndpoint).toString()).toContain(`/index.html?${queryString}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
|
|
||||||
export function getMongoShellUrl(): string {
|
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): 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 `/mongoshell/index.html?${queryString}`;
|
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -375,7 +375,6 @@ 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(),
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
|
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";
|
||||||
@@ -30,6 +34,10 @@ 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(() => {
|
||||||
@@ -40,8 +48,28 @@ 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) => (
|
||||||
@@ -273,11 +301,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
<ConnectTab />
|
<ConnectTab />
|
||||||
);
|
);
|
||||||
case ReactTabKind.Home:
|
case ReactTabKind.Home:
|
||||||
if (isFabricNative()) {
|
return <SplashScreen explorer={explorer} />;
|
||||||
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} />
|
||||||
@@ -290,3 +314,57 @@ 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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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";
|
||||||
@@ -35,6 +34,7 @@ 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,8 +58,6 @@ 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>;
|
||||||
@@ -126,8 +124,6 @@ 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) => {
|
||||||
@@ -214,7 +210,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showScriptsMenus: boolean =
|
const showScriptsMenus: boolean =
|
||||||
!isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
configContext.platform != Platform.Fabric && (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);
|
||||||
|
|||||||
@@ -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/AddCollectionPanel";
|
import { AddCollectionPanel } from "../Panes/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";
|
||||||
|
|||||||
@@ -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 { Collection } from "Contracts/ViewModels";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
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,7 +11,6 @@ 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";
|
||||||
@@ -61,7 +60,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 as Collection)
|
? createResourceTokenTreeNodes(resourceTokenCollection)
|
||||||
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
: createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab);
|
||||||
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
}, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]);
|
||||||
|
|
||||||
@@ -77,22 +76,23 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
|||||||
: [];
|
: [];
|
||||||
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
|
}, [isSampleDataEnabled, sampleDataResourceTokenCollection]);
|
||||||
|
|
||||||
const headerNodes: TreeNode[] = isFabricMirrored()
|
const headerNodes: TreeNode[] =
|
||||||
? []
|
configContext.platform === Platform.Fabric
|
||||||
: [
|
? []
|
||||||
{
|
: [
|
||||||
id: "home",
|
{
|
||||||
iconSrc: <Home16Regular />,
|
id: "home",
|
||||||
label: "Home",
|
iconSrc: <Home16Regular />,
|
||||||
isSelected: () =>
|
label: "Home",
|
||||||
useSelectedNode.getState().selectedNode === undefined &&
|
isSelected: () =>
|
||||||
useTabs.getState().activeReactTab === ReactTabKind.Home,
|
useSelectedNode.getState().selectedNode === undefined &&
|
||||||
onClick: () => {
|
useTabs.getState().activeReactTab === ReactTabKind.Home,
|
||||||
useSelectedNode.getState().setSelectedNode(undefined);
|
onClick: () => {
|
||||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
|
useSelectedNode.getState().setSelectedNode(undefined);
|
||||||
|
useTabs.getState().openAndActivateReactTab(ReactTabKind.Home);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const rootNodes: TreeNode[] = useMemo(() => {
|
const rootNodes: TreeNode[] = useMemo(() => {
|
||||||
if (sampleDataNodes.length > 0) {
|
if (sampleDataNodes.length > 0) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
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 non read-only 1`] = `
|
exports[`createDatabaseTreeNodes generates the correct tree structure for the SQL API, on Fabric 1`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"children": [
|
"children": [
|
||||||
@@ -753,14 +753,8 @@ 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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -780,14 +774,8 @@ 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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -834,14 +822,8 @@ 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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -888,14 +870,8 @@ 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": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -939,145 +915,6 @@ 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`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -1135,7 +972,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "mockSproc4",
|
"label": "mockSproc3",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1153,7 +990,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "mockUdf4",
|
"label": "mockUdf3",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1171,7 +1008,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"isSelected": [Function],
|
"isSelected": [Function],
|
||||||
"label": "mockTrigger4",
|
"label": "mockTrigger3",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1208,7 +1045,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1311,7 +1148,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1445,7 +1282,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1625,7 +1462,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1799,7 +1636,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -1897,7 +1734,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2031,7 +1868,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2211,7 +2048,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe
|
|||||||
"styleClass": "deleteCollectionMenuItem",
|
"styleClass": "deleteCollectionMenuItem",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
@@ -2266,7 +2103,7 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = `
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"iconSrc": <EyeRegular
|
"iconSrc": <DocumentMultipleRegular
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
/>,
|
/>,
|
||||||
"isExpanded": true,
|
"isExpanded": true,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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";
|
||||||
@@ -17,7 +16,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 { FabricContext, updateUserContext, UserContext } from "UserContext";
|
import { updateUserContext } 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";
|
||||||
@@ -82,7 +81,6 @@ jest.mock("Explorer/Tree/Trigger", () => {
|
|||||||
jest.mock("Common/DatabaseAccountUtility", () => {
|
jest.mock("Common/DatabaseAccountUtility", () => {
|
||||||
return {
|
return {
|
||||||
isPublicInternetAccessAllowed: () => true,
|
isPublicInternetAccessAllowed: () => true,
|
||||||
isMaterializedViewsEnabled: () => false,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,15 +133,6 @@ 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([]),
|
||||||
@@ -371,30 +360,9 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
|
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>]>([
|
||||||
[
|
["the SQL API, on Fabric", Platform.Fabric, false, { capabilities: [], enableMultipleWriteLocations: true }],
|
||||||
"the SQL API, on Fabric read-only",
|
["the SQL API, on Portal", Platform.Portal, false, { capabilities: [], enableMultipleWriteLocations: true }],
|
||||||
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,
|
||||||
@@ -405,7 +373,6 @@ 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",
|
||||||
@@ -414,31 +381,26 @@ describe("createDatabaseTreeNodes", () => {
|
|||||||
{
|
{
|
||||||
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
|
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
|
||||||
},
|
},
|
||||||
{ fabricContext: undefined },
|
|
||||||
],
|
],
|
||||||
])(
|
])("generates the correct tree structure for %s", (_, platform, isNotebookEnabled, dbAccountProperties) => {
|
||||||
"generates the correct tree structure for %s",
|
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
|
||||||
(_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
|
updateConfigContext({ platform });
|
||||||
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
|
updateUserContext({
|
||||||
updateConfigContext({ platform });
|
databaseAccount: {
|
||||||
updateUserContext({
|
properties: {
|
||||||
...userContext,
|
enableMultipleWriteLocations: true,
|
||||||
databaseAccount: {
|
...dbAccountProperties,
|
||||||
properties: {
|
},
|
||||||
enableMultipleWriteLocations: true,
|
} as unknown as DataModels.DatabaseAccount,
|
||||||
...dbAccountProperties,
|
});
|
||||||
},
|
const nodes = createDatabaseTreeNodes(
|
||||||
} as unknown as DataModels.DatabaseAccount,
|
explorer,
|
||||||
});
|
isNotebookEnabled,
|
||||||
const nodes = createDatabaseTreeNodes(
|
useDatabases.getState().databases,
|
||||||
explorer,
|
refreshActiveTab,
|
||||||
isNotebookEnabled,
|
);
|
||||||
useDatabases.getState().databases,
|
expect(nodes).toMatchSnapshot();
|
||||||
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.
|
||||||
@@ -589,18 +551,7 @@ 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",
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons";
|
import { DatabaseRegular, DocumentMultipleRegular, 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,7 +6,6 @@ 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";
|
||||||
@@ -23,13 +22,14 @@ import { useNotebook } from "../Notebook/useNotebook";
|
|||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
|
|
||||||
export const shouldShowScriptNodes = (): boolean => {
|
export const shouldShowScriptNodes = (): boolean => {
|
||||||
return !isFabricMirrored() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
return (
|
||||||
|
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.Collection): TreeNode[] => {
|
export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => {
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -111,7 +111,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.Collection):
|
|||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
children,
|
children,
|
||||||
className: "collectionNode",
|
className: "collectionNode",
|
||||||
iconSrc: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
iconSrc: 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: collection.materializedViewDefinition() ? MaterializedViewCollectionIcon : TreeCollectionIcon,
|
iconSrc: TreeCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(collection);
|
useSelectedNode.getState().setSelectedNode(collection);
|
||||||
collection.openTab();
|
collection.openTab();
|
||||||
|
|||||||
@@ -1,112 +1,56 @@
|
|||||||
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 { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||||
import { FabricArtifactInfo, updateUserContext, userContext } from "UserContext";
|
import { 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 | undefined;
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
|
// Prevents multiple parallel requests during DEBOUNCE_DELAY_MS
|
||||||
let lastRequestTimestamp: number | undefined = undefined;
|
let lastRequestTimestamp: number = 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 {
|
||||||
if (isFabricMirrored()) {
|
const fabricDatabaseConnectionInfo = await sendCachedDataMessage<FabricDatabaseConnectionInfo>(
|
||||||
await requestAndStoreDatabaseResourceTokens();
|
FabricMessageTypes.GetAllResourceTokens,
|
||||||
} else if (isFabricNative()) {
|
[],
|
||||||
await requestAndStoreAccessToken();
|
userContext.fabricContext.connectionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userContext.databaseAccount.properties.documentEndpoint) {
|
||||||
|
userContext.databaseAccount.properties.documentEndpoint = fabricDatabaseConnectionInfo.endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleRefreshFabricToken();
|
updateUserContext({
|
||||||
|
fabricContext: {
|
||||||
|
...userContext.fabricContext,
|
||||||
|
databaseConnectionInfo: fabricDatabaseConnectionInfo,
|
||||||
|
isReadOnly: true,
|
||||||
|
},
|
||||||
|
databaseAccount: { ...userContext.databaseAccount },
|
||||||
|
});
|
||||||
|
scheduleRefreshDatabaseResourceToken();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(error as string);
|
logConsoleError(error);
|
||||||
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 scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void> => {
|
export const scheduleRefreshDatabaseResourceToken = (refreshNow?: boolean): Promise<void> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (timeoutId !== undefined) {
|
if (timeoutId !== undefined) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@@ -115,7 +59,7 @@ export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void>
|
|||||||
|
|
||||||
timeoutId = setTimeout(
|
timeoutId = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
requestFabricToken().then(resolve);
|
requestDatabaseResourceTokens().then(resolve);
|
||||||
},
|
},
|
||||||
refreshNow ? 0 : TOKEN_VALIDITY_MS,
|
refreshNow ? 0 : TOKEN_VALIDITY_MS,
|
||||||
);
|
);
|
||||||
@@ -124,15 +68,6 @@ export const scheduleRefreshFabricToken = (refreshNow?: boolean): Promise<void>
|
|||||||
|
|
||||||
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()) {
|
||||||
scheduleRefreshFabricToken(true);
|
scheduleRefreshDatabaseResourceToken(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFabric = (): boolean => configContext.platform === Platform.Fabric;
|
|
||||||
export const isFabricMirroredKey = (): boolean =>
|
|
||||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_KEY;
|
|
||||||
export const isFabricMirroredAAD = (): boolean =>
|
|
||||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_AAD;
|
|
||||||
export const isFabricMirrored = (): boolean => isFabricMirroredKey() || isFabricMirroredAAD();
|
|
||||||
export const isFabricNative = (): boolean =>
|
|
||||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.NATIVE;
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
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 { HttpHeaders } from "../../../Common/Constants";
|
import { BackendApi, 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 {
|
||||||
@@ -17,6 +19,10 @@ 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";
|
||||||
@@ -29,10 +35,28 @@ 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;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ 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;
|
||||||
@@ -82,7 +81,6 @@ 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"),
|
||||||
|
|||||||
@@ -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(selfServeType);
|
await loadTranslations(selfServeExample.constructor.name);
|
||||||
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(selfServeType);
|
await loadTranslations(sqlX.constructor.name);
|
||||||
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(selfServeType);
|
await loadTranslations(graphAPICompute.constructor.name);
|
||||||
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(selfServeType);
|
await loadTranslations(materializedViewsBuilder.constructor.name);
|
||||||
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.toLocaleLowerCase() as keyof typeof SelfServeType];
|
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType];
|
||||||
if (
|
if (
|
||||||
!inputs.subscriptionId ||
|
!inputs.subscriptionId ||
|
||||||
!inputs.resourceGroup ||
|
!inputs.resourceGroup ||
|
||||||
|
|||||||
@@ -29,11 +29,10 @@ 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
|
||||||
// NOTE: text and casing of the enum's value must match the corresponding file in Localization\en\
|
example = "example",
|
||||||
example = "SelfServeExample",
|
sqlx = "sqlx",
|
||||||
sqlx = "SqlX",
|
graphapicompute = "graphapicompute",
|
||||||
graphapicompute = "GraphAPICompute",
|
materializedviewsbuilder = "materializedviewsbuilder",
|
||||||
materializedviewsbuilder = "MaterializedViewsBuilder",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
export enum Action {
|
export enum Action {
|
||||||
CollapseTreeNode,
|
CollapseTreeNode,
|
||||||
CreateCollection,
|
CreateCollection,
|
||||||
CreateMaterializedView,
|
|
||||||
CreateDocument,
|
CreateDocument,
|
||||||
CreateStoredProcedure,
|
CreateStoredProcedure,
|
||||||
CreateTrigger,
|
CreateTrigger,
|
||||||
@@ -120,7 +119,6 @@ export enum Action {
|
|||||||
NotebooksGalleryPublishedCount,
|
NotebooksGalleryPublishedCount,
|
||||||
SelfServe,
|
SelfServe,
|
||||||
ExpandAddCollectionPaneAdvancedSection,
|
ExpandAddCollectionPaneAdvancedSection,
|
||||||
ExpandAddMaterializedViewPaneAdvancedSection,
|
|
||||||
SchemaAnalyzerClickAnalyze,
|
SchemaAnalyzerClickAnalyze,
|
||||||
SelfServeComponent,
|
SelfServeComponent,
|
||||||
LaunchQuickstart,
|
LaunchQuickstart,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CosmosDbArtifactType, ResourceTokenInfo } from "Contracts/FabricMessagesContract";
|
import { FabricDatabaseConnectionInfo } from "Contracts/FabricMessagesContract";
|
||||||
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
import { ParsedResourceTokenConnectionString } from "Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||||
@@ -47,21 +47,11 @@ export interface VCoreMongoConnectionParams {
|
|||||||
connectionString: string;
|
connectionString: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FabricArtifactInfo {
|
interface FabricContext {
|
||||||
[CosmosDbArtifactType.MIRRORED_KEY]: {
|
connectionId: string;
|
||||||
connectionId: string;
|
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
|
||||||
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 =
|
||||||
@@ -80,7 +70,7 @@ export type AdminFeedbackPolicySettings = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface UserContext {
|
export interface UserContext {
|
||||||
readonly fabricContext?: FabricContext<CosmosDbArtifactType>;
|
readonly fabricContext?: FabricContext;
|
||||||
readonly authType?: AuthType;
|
readonly authType?: AuthType;
|
||||||
readonly masterKey?: string;
|
readonly masterKey?: string;
|
||||||
readonly subscriptionId?: string;
|
readonly subscriptionId?: string;
|
||||||
|
|||||||
@@ -89,7 +89,3 @@ export const getItemName = (): string => {
|
|||||||
return "Items";
|
return "Items";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
|
||||||
return apiType === "SQL" || apiType === "Tables";
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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) {
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
import {
|
||||||
|
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(
|
||||||
@@ -66,6 +73,9 @@ 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[] } = {
|
||||||
@@ -90,6 +100,14 @@ 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,
|
||||||
@@ -135,3 +153,53 @@ 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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 => {
|
||||||
@@ -8,7 +7,7 @@ export const getDataExplorerWindow = (currentWindow: Window): Window | undefined
|
|||||||
if (currentWindow.parent === currentWindow) {
|
if (currentWindow.parent === currentWindow) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (isFabric() && currentWindow.parent.parent === currentWindow.top) {
|
if (configContext.platform === Platform.Fabric && 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-12-01-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-02-15-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-12-01-preview";
|
const apiVersion = "2024-02-15-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(
|
||||||
|
|||||||
@@ -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-12-01-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-02-15-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-12-01-preview";
|
const apiVersion = "2024-02-15-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(
|
||||||
|
|||||||
@@ -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-12-01-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-02-15-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-12-01-preview";
|
const apiVersion = "2024-02-15-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(
|
||||||
|
|||||||
@@ -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-12-01-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-02-15-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-12-01-preview";
|
const apiVersion = "2024-02-15-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(
|
||||||
|
|||||||
@@ -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-12-01-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-02-15-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-12-01-preview";
|
const apiVersion = "2024-02-15-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(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user