Compare commits

..

1 Commits

Author SHA1 Message Date
Sampath
3ada04da73 heading role along with level of heading added to the element for the screen reader 2024-02-13 23:46:43 +05:30
132 changed files with 787 additions and 3518 deletions

3
.gitignore vendored
View File

@@ -16,5 +16,4 @@ Contracts/*
.env
failure.png
screenshots/*
GettingStarted-ignore*.ipynb
.vscode/
GettingStarted-ignore*.ipynb

View File

@@ -20,8 +20,8 @@
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode"

View File

@@ -1,5 +1,5 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled": true,
"isPhoenixEnabled": true
}
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
"isTerminalEnabled" : true,
"isPhoenixEnabled" : true
}

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.31299 1.26164C7.69849 0.897163 8.30151 0.897163 8.68701 1.26164L13.5305 5.84098C13.8302 6.12431 14 6.51853 14 6.93094V12.5002C14 13.3286 13.3284 14.0002 12.5 14.0002H10.5C9.67157 14.0002 9 13.3286 9 12.5002V10.0002C9 9.72407 8.77614 9.50021 8.5 9.50021H7.5C7.22386 9.50021 7 9.72407 7 10.0002V12.5002C7 13.3286 6.32843 14.0002 5.5 14.0002H3.5C2.67157 14.0002 2 13.3286 2 12.5002V6.93094C2 6.51853 2.1698 6.12431 2.46948 5.84098L7.31299 1.26164ZM8 1.98828L3.15649 6.56762C3.0566 6.66207 3 6.79347 3 6.93094V12.5002C3 12.7763 3.22386 13.0002 3.5 13.0002H5.5C5.77614 13.0002 6 12.7763 6 12.5002V10.0002C6 9.17179 6.67157 8.50022 7.5 8.50022H8.5C9.32843 8.50022 10 9.17179 10 10.0002V12.5002C10 12.7763 10.2239 13.0002 10.5 13.0002H12.5C12.7761 13.0002 13 12.7763 13 12.5002V6.93094C13 6.79347 12.9434 6.66207 12.8435 6.56762L8 1.98828Z" fill="#0078D4" />
</svg>

Before

Width:  |  Height:  |  Size: 968 B

View File

@@ -163,7 +163,6 @@
/**********************************************************************************/
@FabricFont: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
@FabricToolbarIconColor: "brightness(0) saturate(100%) invert(50%) sepia(17%) saturate(1459%) hue-rotate(81deg) brightness(99%) contrast(94%)";
@FabricBoxBorderRadius: 8px;
@FabricBoxBorderShadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;

View File

@@ -25,30 +25,29 @@ a:focus {
}
.resourceTreeAndTabs {
border-radius: 0px;
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px;
margin-bottom: 0px;
margin-top: 4px;
background-color: #ffffff;
}
.tabsManagerContainer {
background-color: #ffffff
background-color: #fafafa
}
.nav-tabs-margin {
padding-top: 8px;
background-color: #ffffff
background-color: #fafafa
}
.commandBarContainer {
background-color: #ffffff;
border-radius: @FabricBoxBorderRadius @FabricBoxBorderRadius 0px 0px;
border-bottom: none;
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px;
margin-bottom: 0px;
padding-top: 2px;
padding: 0px;
height: 40px;
@@ -163,10 +162,9 @@ a:focus {
.dataExplorerErrorConsoleContainer {
border-radius: 0px 0px @FabricBoxBorderRadius @FabricBoxBorderRadius;
border-radius: @FabricBoxBorderRadius;
box-shadow: @FabricBoxBorderShadow;
margin: @FabricBoxMargin;
margin-top: 0px;
width: auto;
align-self: auto;
}

View File

@@ -78,8 +78,8 @@
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
"p-retry": "4.6.2",
"patch-package": "8.0.0",
"p-retry": "4.6.2",
"plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",
"q": "1.5.1",
@@ -238,4 +238,4 @@
"printWidth": 120,
"endOfLine": "auto"
}
}
}

View File

@@ -124,35 +124,7 @@ export enum MongoBackendEndpointType {
remote,
}
export enum BackendApi {
GenerateToken,
}
export class PortalBackendEndpoints {
public static readonly Development: string = "https://localhost:7235";
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-pbe.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-pbe.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-pbe.cosmos.azure.cn";
}
export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238";
public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
}
export class CassandraProxyEndpoints {
public static readonly Development: string = "https://localhost:7240";
public static readonly Mpac: string = "https://cdb-ms-mpac-cp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-cp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-cp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-cp.cosmos.azure.cn";
}
//TODO: Remove this when new backend is migrated over
// TODO: 435619 Add default endpoints per cloud and use regional only when available
export class CassandraBackend {
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
@@ -164,17 +136,6 @@ export class CassandraBackend {
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
}
export class CassandraProxyAPIs {
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
public static readonly connectionStringCreateOrDeleteApi: string = "api/connectionstring/cassandra/createordelete";
public static readonly queryApi: string = "api/cassandra";
public static readonly connectionStringQueryApi: string = "api/connectionstring/cassandra";
public static readonly keysApi: string = "api/cassandra/keys";
public static readonly connectionStringKeysApi: string = "api/connectionstring/cassandra/keys";
public static readonly schemaApi: string = "api/cassandra/schema";
public static readonly connectionStringSchemaApi: string = "api/connectionstring/cassandra/schema";
}
export class Queries {
public static CustomPageOption: string = "custom";
public static UnlimitedPageOption: string = "unlimited";

View File

@@ -1,6 +1,7 @@
import * as Cosmos from "@azure/cosmos";
import { sendCachedDataMessage } from "Common/MessageHandler";
import { getAuthorizationTokenUsingResourceTokens } from "Common/getAuthorizationTokenUsingResourceTokens";
import { AuthorizationToken } from "Contracts/MessageTypes";
import { AuthorizationToken, MessageTypes } from "Contracts/MessageTypes";
import { checkDatabaseResourceTokensValidity } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { AuthType } from "../AuthType";
@@ -50,23 +51,15 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
case Cosmos.ResourceType.offer:
case Cosmos.ResourceType.user:
case Cosmos.ResourceType.permission:
// For now, these operations aren't used, so fetching the authorization token is commented out.
// This provider must return a real token to pass validation by the client, so we return the cached resource token
// (which is a valid token, but won't work for these operations).
const resourceTokens2 = userContext.fabricContext.databaseConnectionInfo.resourceTokens;
return getAuthorizationTokenUsingResourceTokens(resourceTokens2, requestInfo.path, requestInfo.resourceId);
/* ************** TODO: Uncomment this code if we need to support these operations **************
// User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
MessageTypes.GetAuthorizationToken,
[requestInfo],
userContext.fabricContext.connectionId,
);
console.log("Response from Fabric: ", authorizationToken);
headers[HttpHeaders.msDate] = authorizationToken.XDate;
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
***********************************************************************************************/
// User master tokens
const authorizationToken = await sendCachedDataMessage<AuthorizationToken>(
MessageTypes.GetAuthorizationToken,
[requestInfo],
userContext.fabricContext.connectionId,
);
console.log("Response from Fabric: ", authorizationToken);
headers[HttpHeaders.msDate] = authorizationToken.XDate;
return decodeURIComponent(authorizationToken.PrimaryReadWriteToken);
}
}
@@ -85,8 +78,6 @@ export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
return decodeURIComponent(result.PrimaryReadWriteToken);
};
// TODO
// Need to create separate plugins or change endpoint logic to return the correct write or read endpoint.
export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, diagnosticNode, next) => {
requestContext.endpoint = new URL(configContext.PROXY_PATH, window.location.href).href;
requestContext.headers["x-ms-proxy-target"] = endpoint();
@@ -136,32 +127,9 @@ enum SDKSupportedCapabilities {
PartitionMerge = 1 << 0,
}
// Client Management
let _client: Cosmos.CosmosClient;
let _readClients: Map<string, Cosmos.CosmosClient> = new Map();
export enum ClientOperationType {
READ,
WRITE,
}
export function client(clientOperationType: ClientOperationType): Cosmos.CosmosClient {
switch (clientOperationType) {
case ClientOperationType.READ:
return readClients();
case ClientOperationType.WRITE:
return writeClient();
default:
throw new Error("Invalid operation type");
}
}
export function writeClient(): Cosmos.CosmosClient {
console.log(`Called primary client`);
const currentUserContextDocumentEndpoint = userContext?.databaseAccount?.properties?.documentEndpoint;
console.log(`Current selected endpoint in userContext: ${currentUserContextDocumentEndpoint}`);
export function client(): Cosmos.CosmosClient {
if (_client) return _client;
let _defaultHeaders: Cosmos.CosmosHeaders = {};
@@ -208,61 +176,3 @@ export function writeClient(): Cosmos.CosmosClient {
_client = new Cosmos.CosmosClient(options);
return _client;
}
export function readClients(): Cosmos.CosmosClient {
console.log(`Called read only client`);
const currentUserContextDocumentEndpoint = userContext?.databaseAccount?.properties?.documentEndpoint;
console.log(`Current selected read endpoint in userContext: ${currentUserContextDocumentEndpoint}`);
3;
const selectedEndpoint = endpoint() || "https://cosmos.azure.com";
if (_readClients.has(selectedEndpoint)) {
return _readClients.get(selectedEndpoint);
}
let _defaultHeaders: Cosmos.CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supportedcapabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
if (
userContext.authType === AuthType.ConnectionString ||
userContext.authType === AuthType.EncryptedToken ||
userContext.authType === AuthType.ResourceToken
) {
// Default to low priority. Needed for non-AAD-auth scenarios
// where we cannot use RP API, and thus, cannot detect whether priority
// based execution is enabled.
// The header will be ignored if priority based execution is disabled on the account.
_defaultHeaders["x-ms-cosmos-priority-level"] = PriorityLevel.Default;
}
const options: Cosmos.CosmosClientOptions = {
endpoint: selectedEndpoint, // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey,
tokenProvider,
userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders,
connectionPolicy: {
retryOptions: {
maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts),
fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval),
maxWaitTimeInSeconds: LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds),
},
},
};
if (configContext.PROXY_PATH !== undefined) {
(options as any).plugins = [{ on: "request", plugin: requestPlugin }];
}
if (PriorityBasedExecutionUtils.isFeatureEnabled()) {
const plugins = (options as any).plugins || [];
plugins.push({ on: "request", plugin: PriorityBasedExecutionUtils.requestPlugin });
(options as any).plugins = plugins;
}
_readClients.set(selectedEndpoint, new Cosmos.CosmosClient(options));
return _readClients.get(selectedEndpoint);
}

View File

@@ -1,9 +1,5 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import {
allowedMongoProxyEndpoints,
allowedMongoProxyEndpoints_ToBeDeprecated,
validateEndpoint,
} from "Utils/EndpointUtils";
import { MongoProxyEndpoints, allowedMongoProxyEndpoints_ToBeDeprecated, validateEndpoint } from "Utils/EndpointUtils";
import queryString from "querystring";
import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext";
@@ -14,7 +10,7 @@ import DocumentId from "../Explorer/Tree/DocumentId";
import { hasFlag } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes, MongoProxyEndpoints } from "./Constants";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities";
import { sendMessage } from "./MessageHandler";
@@ -465,7 +461,7 @@ export function updateDocument_ToBeDeprecated(
export function deleteDocument(databaseId: string, collection: Collection, documentId: DocumentId): Promise<void> {
if (!useMongoProxyEndpoint("deleteDocument")) {
return deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
deleteDocument_ToBeDeprecated(databaseId, collection, documentId);
}
const { databaseAccount } = userContext;
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
@@ -554,7 +550,7 @@ export function createMongoCollectionWithProxy(
params: DataModels.CreateCollectionParams,
): Promise<DataModels.Collection> {
if (!useMongoProxyEndpoint("createCollectionWithProxy")) {
return createMongoCollectionWithProxy_ToBeDeprecated(params);
createMongoCollectionWithProxy_ToBeDeprecated(params);
}
const { databaseAccount } = userContext;
const shardKey: string = params.partitionKey?.paths[0];
@@ -648,10 +644,7 @@ export function getFeatureEndpointOrDefault(feature: string): string {
} else {
endpoint =
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
validateEndpoint(userContext.features.mongoProxyEndpoint, [
...allowedMongoProxyEndpoints,
...allowedMongoProxyEndpoints_ToBeDeprecated,
])
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints_ToBeDeprecated)
? userContext.features.mongoProxyEndpoint
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
}
@@ -690,15 +683,8 @@ export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameter
}
function useMongoProxyEndpoint(api: string): boolean {
const activeMongoProxyEndpoints: string[] = [MongoProxyEndpoints.Development];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
canAccessMongoProxy = canAccessMongoProxy && configContext.MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessMongoProxy &&
configContext.NEW_MONGO_APIS?.includes(api) &&
activeMongoProxyEndpoints.includes(configContext.MONGO_PROXY_ENDPOINT)
[MongoProxyEndpoints.Development, MongoProxyEndpoints.MPAC].includes(configContext.MONGO_PROXY_ENDPOINT)
);
}

View File

@@ -1,7 +1,7 @@
import { JSONObject, OperationResponse } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export const bulkCreateDocument = async (
@@ -13,7 +13,7 @@ export const bulkCreateDocument = async (
);
try {
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.bulk(

View File

@@ -6,14 +6,14 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
import { createDatabase } from "./createDatabase";
@@ -284,9 +284,7 @@ const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams
}
}
const databaseResponse: DatabaseResponse = await client(ClientOperationType.WRITE).databases.createIfNotExists(
createDatabaseBody,
);
const databaseResponse: DatabaseResponse = await client().databases.createIfNotExists(createDatabaseBody);
const collectionResponse: ContainerResponse = await databaseResponse?.database.containers.create(
createCollectionBody,
collectionOptions,

View File

@@ -4,7 +4,6 @@ import * as DataModels from "../../Contracts/DataModels";
import { useDatabases } from "../../Explorer/useDatabases";
import { userContext } from "../../UserContext";
import { getDatabaseName } from "../../Utils/APITypeUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateCassandraKeyspace } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
@@ -16,7 +15,8 @@ import {
MongoDBDatabaseCreateUpdateParameters,
SqlDatabaseCreateUpdateParameters,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
@@ -153,7 +153,7 @@ async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): P
}
}
const response: DatabaseResponse = await client(ClientOperationType.WRITE).databases.create(createBody);
const response: DatabaseResponse = await client().databases.create(createBody);
return response.resource;
}

View File

@@ -1,6 +1,6 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
@@ -9,7 +9,7 @@ export const createDocument = async (collection: CollectionBase, newDocument: un
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
try {
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument);

View File

@@ -1,7 +1,6 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import {
createUpdateSqlStoredProcedure,
getSqlStoredProcedure,
@@ -10,7 +9,8 @@ import {
SqlStoredProcedureCreateUpdateParameters,
SqlStoredProcedureResource,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function createStoredProcedure(
@@ -63,7 +63,7 @@ export async function createStoredProcedure(
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
}
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.create(storedProcedure);

View File

@@ -1,10 +1,10 @@
import { TriggerDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function createTrigger(
@@ -55,7 +55,7 @@ export async function createTrigger(
return rpResponse && rpResponse.properties?.resource;
}
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type

View File

@@ -1,7 +1,6 @@
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import {
createUpdateSqlUserDefinedFunction,
getSqlUserDefinedFunction,
@@ -10,7 +9,8 @@ import {
SqlUserDefinedFunctionCreateUpdateParameters,
SqlUserDefinedFunctionResource,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function createUserDefinedFunction(
@@ -63,7 +63,7 @@ export async function createUserDefinedFunction(
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.create(userDefinedFunction);

View File

@@ -1,195 +0,0 @@
import { ApiType, userContext } from "UserContext";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import {
cancel,
create,
get,
listByDatabaseAccount,
} from "Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
import {
CosmosCassandraDataTransferDataSourceSink,
CosmosMongoDataTransferDataSourceSink,
CosmosSqlDataTransferDataSourceSink,
CreateJobRequest,
DataTransferJobFeedResults,
DataTransferJobGetResults,
} from "Utils/arm/generatedClients/dataTransferService/types";
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
export interface DataTransferParams {
jobName: string;
apiType: ApiType;
subscriptionId: string;
resourceGroupName: string;
accountName: string;
sourceDatabaseName: string;
sourceCollectionName: string;
targetDatabaseName: string;
targetCollectionName: string;
}
export const getDataTransferJobs = async (
subscriptionId: string,
resourceGroup: string,
accountName: string,
): Promise<DataTransferJobGetResults[]> => {
let dataTransferJobs: DataTransferJobGetResults[] = [];
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
subscriptionId,
resourceGroup,
accountName,
);
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
while (dataTransferFeeds?.nextLink) {
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
headers: {
Authorization: userContext.authorizationToken,
},
});
if (nextResponse.ok) {
dataTransferFeeds = await nextResponse.json();
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
} else {
break;
}
}
return dataTransferJobs;
};
export const initiateDataTransfer = async (params: DataTransferParams): Promise<DataTransferJobGetResults> => {
const {
jobName,
apiType,
subscriptionId,
resourceGroupName,
accountName,
sourceDatabaseName,
sourceCollectionName,
targetDatabaseName,
targetCollectionName,
} = params;
const sourcePayload = createPayload(apiType, sourceDatabaseName, sourceCollectionName);
const targetPayload = createPayload(apiType, targetDatabaseName, targetCollectionName);
const body: CreateJobRequest = {
properties: {
source: sourcePayload,
destination: targetPayload,
},
};
return create(subscriptionId, resourceGroupName, accountName, jobName, body);
};
export const pollDataTransferJob = async (
jobName: string,
subscriptionId: string,
resourceGroupName: string,
accountName: string,
): Promise<unknown> => {
const currentPollingJobs = useDataTransferJobs.getState().pollingDataTransferJobs;
if (currentPollingJobs.has(jobName)) {
return;
}
let clearMessage = NotificationConsoleUtils.logConsoleProgress(`Data transfer job ${jobName} in progress`);
return await promiseRetry(
() => pollDataTransferJobOperation(jobName, subscriptionId, resourceGroupName, accountName, clearMessage),
{
retries: 500,
maxTimeout: 5000,
onFailedAttempt: (error: FailedAttemptError) => {
clearMessage();
clearMessage = NotificationConsoleUtils.logConsoleProgress(error.message);
},
},
);
};
const pollDataTransferJobOperation = async (
jobName: string,
subscriptionId: string,
resourceGroupName: string,
accountName: string,
clearMessage?: () => void,
): Promise<DataTransferJobGetResults> => {
if (!userContext.authorizationToken) {
throw new Error("No authority token provided");
}
addToPolling(jobName);
const body: DataTransferJobGetResults = await get(subscriptionId, resourceGroupName, accountName, jobName);
const status = body?.properties?.status;
updateDataTransferJob(body);
if (status === "Cancelled") {
removeFromPolling(jobName);
clearMessage && clearMessage();
const cancelMessage = `Data transfer job ${jobName} cancelled`;
NotificationConsoleUtils.logConsoleError(cancelMessage);
throw new AbortError(cancelMessage);
}
if (status === "Failed" || status === "Faulted") {
removeFromPolling(jobName);
const errorMessage = body?.properties?.error
? JSON.stringify(body?.properties?.error)
: "Operation could not be completed";
const error = new Error(errorMessage);
clearMessage && clearMessage();
NotificationConsoleUtils.logConsoleError(`Data transfer job ${jobName} failed: ${errorMessage}`);
throw new AbortError(error);
}
if (status === "Completed") {
removeFromPolling(jobName);
clearMessage && clearMessage();
NotificationConsoleUtils.logConsoleInfo(`Data transfer job ${jobName} completed`);
return body;
}
const processedCount = body.properties.processedCount;
const totalCount = body.properties.totalCount;
const retryMessage = `Data transfer job ${jobName} in progress, total count: ${totalCount}, processed count: ${processedCount}`;
throw new Error(retryMessage);
};
export const cancelDataTransferJob = async (
subscriptionId: string,
resourceGroupName: string,
accountName: string,
jobName: string,
): Promise<void> => {
const cancelResult: DataTransferJobGetResults = await cancel(subscriptionId, resourceGroupName, accountName, jobName);
updateDataTransferJob(cancelResult);
removeFromPolling(cancelResult?.properties?.jobName);
};
const createPayload = (
apiType: ApiType,
databaseName: string,
containerName: string,
):
| CosmosSqlDataTransferDataSourceSink
| CosmosMongoDataTransferDataSourceSink
| CosmosCassandraDataTransferDataSourceSink => {
switch (apiType) {
case "SQL":
return {
component: "CosmosDBSql",
databaseName: databaseName,
containerName: containerName,
} as CosmosSqlDataTransferDataSourceSink;
case "Mongo":
return {
component: "CosmosDBMongo",
databaseName: databaseName,
collectionName: containerName,
} as CosmosMongoDataTransferDataSourceSink;
case "Cassandra":
return {
component: "CosmosDBCassandra",
keyspaceName: databaseName,
tableName: containerName,
};
default:
throw new Error(`Unsupported API type for data transfer: ${apiType}`);
}
};

View File

@@ -6,7 +6,7 @@ import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { deleteTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client, ClientOperationType } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
@@ -15,7 +15,7 @@ export async function deleteCollection(databaseId: string, collectionId: string)
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
await deleteCollectionWithARM(databaseId, collectionId);
} else {
await client(ClientOperationType.WRITE).database(databaseId).container(collectionId).delete();
await client().database(databaseId).container(collectionId).delete();
}
logConsoleInfo(`Successfully deleted container ${collectionId}`);
} catch (error) {

View File

@@ -1,9 +1,9 @@
import { RequestOptions } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import ConflictId from "../../Explorer/Tree/ConflictId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientOperationType, client } from "../CosmosClient";
import { CollectionBase } from "../../Contracts/ViewModels";
import { RequestOptions } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<void> => {
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
@@ -13,7 +13,7 @@ export const deleteConflict = async (collection: CollectionBase, conflictId: Con
partitionKey: getPartitionKeyHeaderForConflict(conflictId),
};
await client(ClientOperationType.WRITE)
await client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())

View File

@@ -5,7 +5,7 @@ import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/cosmos/g
import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client, ClientOperationType } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function deleteDatabase(databaseId: string): Promise<void> {
@@ -15,7 +15,7 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
if (userContext.authType === AuthType.AAD && !userContext.features.enableSDKoperations) {
await deleteDatabaseWithARM(databaseId);
} else {
await client(ClientOperationType.WRITE).database(databaseId).delete();
await client().database(databaseId).delete();
}
logConsoleInfo(`Successfully deleted database ${databaseId}`);
} catch (error) {

View File

@@ -1,7 +1,7 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
@@ -11,7 +11,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
try {
await client(ClientOperationType.WRITE)
await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), getPartitionKeyValue(documentId))

View File

@@ -2,7 +2,7 @@ import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { deleteSqlStoredProcedure } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client, ClientOperationType } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function deleteStoredProcedure(
@@ -26,11 +26,7 @@ export async function deleteStoredProcedure(
storedProcedureId,
);
} else {
await client(ClientOperationType.WRITE)
.database(databaseId)
.container(collectionId)
.scripts.storedProcedure(storedProcedureId)
.delete();
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
}
} catch (error) {
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);

View File

@@ -2,7 +2,7 @@ import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { deleteSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client, ClientOperationType } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> {
@@ -22,11 +22,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
triggerId,
);
} else {
await client(ClientOperationType.WRITE)
.database(databaseId)
.container(collectionId)
.scripts.trigger(triggerId)
.delete();
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
}
} catch (error) {
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);

View File

@@ -2,7 +2,7 @@ import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { deleteSqlUserDefinedFunction } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client, ClientOperationType } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> {
@@ -22,11 +22,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
id,
);
} else {
await client(ClientOperationType.WRITE)
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunction(id)
.delete();
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
}
} catch (error) {
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);

View File

@@ -1,9 +1,9 @@
import { Collection } from "../../Contracts/ViewModels";
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientDefaults, HttpHeaders } from "../Constants";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
export interface ExecuteSprocResult {
result: StoredProcedure;
@@ -22,7 +22,7 @@ export const executeStoredProcedure = async (
}, ClientDefaults.requestTimeoutMs);
try {
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())

View File

@@ -1,9 +1,9 @@
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import * as Constants from "../Constants";
import { ClientOperationType, client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise<number> {
if (userContext.authType !== AuthType.AAD) {
@@ -12,10 +12,7 @@ export async function getIndexTransformationProgress(databaseId: string, collect
let indexTransformationPercentage: number;
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
try {
const response = await client(ClientOperationType.READ)
.database(databaseId)
.container(collectionId)
.read({ populateQuotaInfo: true });
const response = await client().database(databaseId).container(collectionId).read({ populateQuotaInfo: true });
indexTransformationPercentage = parseInt(
response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string,

View File

@@ -1,5 +1,5 @@
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
export const queryConflicts = (
databaseId: string,
@@ -7,5 +7,5 @@ export const queryConflicts = (
query: string,
options: FeedOptions,
): QueryIterator<ConflictDefinition & Resource> => {
return client(ClientOperationType.READ).database(databaseId).container(containerId).conflicts.query(query, options);
return client().database(databaseId).container(containerId).conflicts.query(query, options);
};

View File

@@ -1,7 +1,7 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
export const queryDocuments = (
databaseId: string,
@@ -10,7 +10,7 @@ export const queryDocuments = (
options: FeedOptions,
): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return client(ClientOperationType.READ).database(databaseId).container(containerId).items.query(query, options);
return client().database(databaseId).container(containerId).items.query(query, options);
};
export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {

View File

@@ -3,11 +3,11 @@ import { sampleDataClient } from "Common/SampleDataClient";
import { userContext } from "UserContext";
import * as DataModels from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
const cosmosClient = client(ClientOperationType.READ);
const cosmosClient = client();
return await readCollectionInternal(cosmosClient, databaseId, collectionId);
}

View File

@@ -10,7 +10,7 @@ import { listGremlinGraphs } from "../../Utils/arm/generatedClients/cosmos/greml
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { listSqlContainers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { listTables } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
@@ -31,7 +31,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
const tokenCollectionId = resourceIdObj[3];
if (tokenDatabaseId === databaseId) {
promises.push(client(ClientOperationType.READ).database(databaseId).container(tokenCollectionId).read());
promises.push(client().database(databaseId).container(tokenCollectionId).read());
}
}
@@ -61,7 +61,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
return await readCollectionsWithARM(databaseId);
}
const sdkResponse = await client(ClientOperationType.READ).database(databaseId).containers.readAll().fetchAll();
const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll();
return sdkResponse.resources as DataModels.Collection[];
} catch (error) {
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
@@ -77,7 +77,7 @@ export async function readCollectionsWithPagination(
): Promise<DataModels.CollectionsWithPagination> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try {
const sdkResponse = await client(ClientOperationType.READ)
const sdkResponse = await client()
.database(databaseId)
.containers.query(
{ query: "SELECT * FROM c" },

View File

@@ -7,7 +7,7 @@ import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { listSqlDatabases } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readDatabases(): Promise<DataModels.Database[]> {
@@ -56,7 +56,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
) {
databases = await readDatabasesWithARM();
} else {
const sdkResponse = await client(ClientOperationType.READ).databases.readAll().fetchAll();
const sdkResponse = await client().databases.readAll().fetchAll();
databases = sdkResponse.resources as DataModels.Database[];
}
} catch (error) {

View File

@@ -3,7 +3,7 @@ import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { HttpHeaders } from "../Constants";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
@@ -19,7 +19,7 @@ export const readDocument = async (collection: CollectionBase, documentId: Docum
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
}
: {};
const response = await client(ClientOperationType.READ)
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), getPartitionKeyValue(documentId))

View File

@@ -1,7 +1,7 @@
import { RequestOptions } from "@azure/cosmos";
import { Offer } from "../../Contracts/DataModels";
import { HttpHeaders } from "../Constants";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readOffers } from "./readOffers";
@@ -21,7 +21,7 @@ export const readOfferWithSDK = async (offerId: string, resourceId: string): Pro
[HttpHeaders.populateCollectionThroughputInfo]: true,
},
};
const response = await client(ClientOperationType.READ).offer(offerId).read(options);
const response = await client().offer(offerId).read(options);
return parseSDKOfferResponse(response);
};

View File

@@ -1,13 +1,13 @@
import { SDKOfferDefinition } from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientOperationType, client } from "../CosmosClient";
import { getErrorMessage, handleError } from "../ErrorHandlingUtils";
import { client } from "../CosmosClient";
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
const clearMessage = logConsoleProgress(`Querying offers`);
try {
const response = await client(ClientOperationType.READ).offers.readAll().fetchAll();
const response = await client().offers.readAll().fetchAll();
return response?.resources;
} catch (error) {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.

View File

@@ -4,7 +4,7 @@ import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readStoredProcedures(
@@ -34,7 +34,7 @@ export async function readStoredProcedures(
throw new Error(cloudError?.error?.message);
}
const response = await client(ClientOperationType.READ)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.readAll()

View File

@@ -1,10 +1,10 @@
import { TriggerDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listSqlTriggers } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readTriggers(
@@ -28,11 +28,7 @@ export async function readTriggers(
return rpResponse?.value?.map((trigger) => trigger.properties?.resource);
}
const response = await client(ClientOperationType.READ)
.database(databaseId)
.container(collectionId)
.scripts.triggers.readAll()
.fetchAll();
const response = await client().database(databaseId).container(collectionId).scripts.triggers.readAll().fetchAll();
return response?.resources;
} catch (error) {
handleError(error, "ReadTriggers", `Failed to query triggers for container ${collectionId}`);

View File

@@ -1,9 +1,9 @@
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listSqlUserDefinedFunctions } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function readUserDefinedFunctions(
@@ -24,7 +24,7 @@ export async function readUserDefinedFunctions(
return rpResponse?.value?.map((udf) => udf.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
const response = await client(ClientOperationType.READ)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.readAll()

View File

@@ -2,7 +2,6 @@ import { ContainerDefinition, RequestOptions } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import {
createUpdateCassandraTable,
getCassandraTable,
@@ -20,7 +19,8 @@ import {
SqlContainerCreateUpdateParameters,
SqlContainerResource,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function updateCollection(
@@ -40,7 +40,7 @@ export async function updateCollection(
) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else {
const sdkResponse = await client(ClientOperationType.WRITE)
const sdkResponse = await client()
.database(databaseId)
.container(collectionId)
.replace(newCollection as ContainerDefinition, options);

View File

@@ -3,7 +3,7 @@ import { HttpHeaders } from "Common/Constants";
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { getPartitionKeyValue } from "./getPartitionKeyValue";
@@ -23,7 +23,7 @@ export const updateDocument = async (
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
}
: {};
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), getPartitionKeyValue(documentId))

View File

@@ -2,7 +2,6 @@ import { OfferDefinition, RequestOptions } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import {
migrateCassandraKeyspaceToAutoscale,
migrateCassandraKeyspaceToManualThroughput,
@@ -41,8 +40,9 @@ import {
updateTableThroughput,
} from "../../Utils/arm/generatedClients/cosmos/tableResources";
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { HttpHeaders } from "../Constants";
import { ClientOperationType, client } from "../CosmosClient";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { parseSDKOfferResponse } from "../OfferUtility";
import { readCollectionOffer } from "./readCollectionOffer";
@@ -401,7 +401,7 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
newOffer.content.offerAutopilotSettings = { maxThroughput: 0 };
}
const sdkResponse = await client(ClientOperationType.WRITE)
const sdkResponse = await client()
.offer(params.currentOffer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.replace(newOffer as unknown as OfferDefinition, options);

View File

@@ -1,7 +1,6 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import {
createUpdateSqlStoredProcedure,
getSqlStoredProcedure,
@@ -10,7 +9,8 @@ import {
SqlStoredProcedureCreateUpdateParameters,
SqlStoredProcedureResource,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function updateStoredProcedure(
@@ -54,7 +54,7 @@ export async function updateStoredProcedure(
throw new Error(`Failed to update stored procedure: ${storedProcedure.id} does not exist.`);
}
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedure(storedProcedure.id)

View File

@@ -1,10 +1,10 @@
import { TriggerDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function updateTrigger(
@@ -47,7 +47,7 @@ export async function updateTrigger(
throw new Error(`Failed to update trigger: ${trigger.id} does not exist.`);
}
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.trigger(trigger.id)

View File

@@ -1,7 +1,6 @@
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import {
createUpdateSqlUserDefinedFunction,
getSqlUserDefinedFunction,
@@ -10,7 +9,8 @@ import {
SqlUserDefinedFunctionCreateUpdateParameters,
SqlUserDefinedFunctionResource,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { ClientOperationType, client } from "../CosmosClient";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
export async function updateUserDefinedFunction(
@@ -53,7 +53,7 @@ export async function updateUserDefinedFunction(
throw new Error(`Failed to update user defined function: ${userDefinedFunction.id} does not exist.`);
}
const response = await client(ClientOperationType.WRITE)
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunction(userDefinedFunction.id)

View File

@@ -1,14 +1,7 @@
import {
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import { JunoEndpoints } from "Common/Constants";
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedCassandraProxyEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
@@ -45,15 +38,9 @@ export interface ConfigContext {
ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
BACKEND_ENDPOINT?: string;
PORTAL_BACKEND_ENDPOINT?: string;
NEW_BACKEND_APIS?: BackendApi[];
MONGO_BACKEND_ENDPOINT?: string;
MONGO_PROXY_ENDPOINT?: string;
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED?: boolean;
NEW_MONGO_APIS?: string[];
CASSANDRA_PROXY_ENDPOINT?: string;
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: boolean;
NEW_CASSANDRA_APIS?: string[];
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string;
@@ -98,9 +85,7 @@ let configContext: Readonly<ConfigContext> = {
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
JUNO_ENDPOINT: JunoEndpoints.Prod,
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
NEW_BACKEND_APIS: [BackendApi.GenerateToken],
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
MONGO_PROXY_ENDPOINT: "https://cdb-ms-prod-mp.cosmos.azure.com",
NEW_MONGO_APIS: [
// "resourcelist",
// "createDocument",
@@ -109,15 +94,6 @@ let configContext: Readonly<ConfigContext> = {
// "deleteDocument",
// "createCollectionWithProxy",
],
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
NEW_CASSANDRA_APIS: [
// "postQuery",
// "createOrDelete",
// "getKeys",
// "getSchema",
],
CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
isTerminalEnabled: false,
isPhoenixEnabled: false,
};
@@ -171,10 +147,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.MONGO_BACKEND_ENDPOINT;
}
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, allowedCassandraProxyEndpoints)) {
delete newContext.CASSANDRA_PROXY_ENDPOINT;
}
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
delete newContext.JUNO_ENDPOINT;
}
@@ -192,7 +164,10 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
// Injected for local development. These will be removed in the production bundle by webpack
if (process.env.NODE_ENV === "development") {
const port: string = process.env.PORT || "1234";
updateConfigContext({
BACKEND_ENDPOINT: "https://localhost:" + port,
MONGO_BACKEND_ENDPOINT: "https://localhost:" + port,
PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: "https://localhost:8081",
});

View File

@@ -159,7 +159,6 @@ export interface Collection extends Resource {
geospatialConfig?: GeospatialConfig;
schema?: ISchema;
requestSchema?: () => void;
computedProperties?: ComputedProperties;
}
export interface CollectionsWithPagination {
@@ -198,13 +197,6 @@ export interface IndexingPolicy {
spatialIndexes?: any;
}
export interface ComputedProperty {
name: string;
query: string;
}
export type ComputedProperties = ComputedProperty[];
export interface PartitionKey {
paths: string[];
kind: "Hash" | "Range" | "MultiHash";

View File

@@ -53,7 +53,6 @@ export type FabricMessageV2 =
id: string;
message: {
connectionId: string;
isVisible: boolean;
};
}
| {
@@ -73,7 +72,7 @@ export type FabricMessageV2 =
};
}
| {
type: "explorerVisible";
type: "setToolbarStatus";
message: {
visible: boolean;
};

View File

@@ -135,7 +135,6 @@ export interface Collection extends CollectionBase {
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
documentIds: ko.ObservableArray<DocumentId>;
computedProperties: ko.Observable<DataModels.ComputedProperties>;
cassandraKeys: CassandraTableKeys;
cassandraSchema: CassandraTableKey[];
@@ -388,7 +387,6 @@ export interface DataExplorerInputsFrame {
serverId?: string;
extensionEndpoint?: string;
mongoProxyEndpoint?: string;
cassandraProxyEndpoint?: string;
subscriptionType?: SubscriptionType;
quotaId?: string;
isTryCosmosDBSubscription?: boolean;
@@ -408,7 +406,6 @@ export interface DataExplorerInputsFrame {
features?: {
[key: string]: string;
};
feedbackPolicies?: any;
}
export interface SelfServeFrameInputs {

View File

@@ -1,10 +1,5 @@
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
import {
ComputedPropertiesComponent,
ComputedPropertiesComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
import { useDatabases } from "Explorer/useDatabases";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
@@ -23,10 +18,6 @@ import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import {
PartitionKeyComponent,
PartitionKeyComponentProps,
} from "../../Controls/Settings/SettingsSubComponents/PartitionKeyComponent";
import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
@@ -112,11 +103,6 @@ export interface SettingsComponentState {
indexesToAdd: AddMongoIndexProps[];
indexTransformationProgress: number;
computedPropertiesContent: DataModels.ComputedProperties;
computedPropertiesContentBaseline: DataModels.ComputedProperties;
shouldDiscardComputedProperties: boolean;
isComputedPropertiesDirty: boolean;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
conflictResolutionPolicyPath: string;
@@ -141,9 +127,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private offer: DataModels.Offer;
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean;
private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource;
@@ -155,9 +139,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@@ -209,11 +191,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isMongoIndexingPolicyDiscardable: false,
indexTransformationProgress: undefined,
computedPropertiesContent: undefined,
computedPropertiesContentBaseline: undefined,
shouldDiscardComputedProperties: false,
isComputedPropertiesDirty: false,
conflictResolutionPolicyMode: undefined,
conflictResolutionPolicyModeBaseline: undefined,
conflictResolutionPolicyPath: undefined,
@@ -304,7 +281,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
);
};
@@ -315,7 +291,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.state.isSubSettingsDiscardable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
);
};
@@ -420,9 +395,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isMongoIndexingPolicySaveable: false,
isMongoIndexingPolicyDiscardable: false,
isConflictResolutionDirty: false,
computedPropertiesContent: this.state.computedPropertiesContentBaseline,
shouldDiscardComputedProperties: true,
isComputedPropertiesDirty: false,
});
};
@@ -542,31 +514,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
this.setState({ isMongoIndexingPolicyDiscardable });
private onComputedPropertiesContentChange = (newComputedProperties: DataModels.ComputedProperties): void =>
this.setState({ computedPropertiesContent: newComputedProperties });
private resetShouldDiscardComputedProperties = (): void => this.setState({ shouldDiscardComputedProperties: false });
private logComputedPropertiesSuccessMessage = (): void => {
if (this.props.settingsTab.onLoadStartKey) {
traceSuccess(
Action.Tab,
{
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
},
this.props.settingsTab.onLoadStartKey,
);
this.props.settingsTab.onLoadStartKey = undefined;
}
};
private onComputedPropertiesDirtyChange = (isComputedPropertiesDirty: boolean): void =>
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
private calculateTotalThroughputUsed = (): void => {
this.totalThroughputUsed = 0;
(useDatabases.getState().databases || []).forEach(async (database) => {
@@ -689,6 +636,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const indexingPolicyContent = this.collection.indexingPolicy();
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
@@ -697,12 +645,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const geospatialConfigTypeString: string =
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
let computedPropertiesContent = this.collection.computedProperties();
if (!computedPropertiesContent || computedPropertiesContent.length === 0) {
computedPropertiesContent = [
{ name: "name_of_property", query: "query_to_compute_property" },
] as DataModels.ComputedProperties;
}
return {
throughput: offerThroughput,
@@ -729,8 +671,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
geospatialConfigType: geoSpatialConfigType,
geospatialConfigTypeBaseline: geoSpatialConfigType,
computedPropertiesContent: computedPropertiesContent,
computedPropertiesContentBaseline: computedPropertiesContent,
};
};
@@ -847,12 +787,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private saveCollectionSettings = async (startKey: number): Promise<void> => {
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
if (
this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
this.state.isComputedPropertiesDirty
) {
if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) {
let defaultTtl: number;
switch (this.state.timeToLive) {
case TtlType.On:
@@ -890,10 +825,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
}
if (this.state.isComputedPropertiesDirty) {
newCollection.computedProperties = this.state.computedPropertiesContent;
}
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
@@ -907,7 +838,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
this.collection.computedProperties(updatedCollection.computedProperties);
if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress();
@@ -918,7 +848,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false,
isComputedPropertiesDirty: false,
});
}
@@ -1113,16 +1042,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
};
const computedPropertiesComponentProps: ComputedPropertiesComponentProps = {
computedPropertiesContent: this.state.computedPropertiesContent,
computedPropertiesContentBaseline: this.state.computedPropertiesContentBaseline,
logComputedPropertiesSuccessMessage: this.logComputedPropertiesSuccessMessage,
onComputedPropertiesContentChange: this.onComputedPropertiesContentChange,
onComputedPropertiesDirtyChange: this.onComputedPropertiesDirtyChange,
resetShouldDiscardComputedProperties: this.resetShouldDiscardComputedProperties,
shouldDiscardComputedProperties: this.state.shouldDiscardComputedProperties,
};
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
collection: this.collection,
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
@@ -1137,12 +1056,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
};
const partitionKeyComponentProps: PartitionKeyComponentProps = {
database: useDatabases.getState().findDatabaseWithId(this.collection.databaseId),
collection: this.collection,
explorer: this.props.settingsTab.getContainer(),
};
const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({
@@ -1178,20 +1091,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
}
if (this.shouldShowPartitionKeyEditor) {
tabs.push({
tab: SettingsV2TabTypes.PartitionKeyTab,
content: <PartitionKeyComponent {...partitionKeyComponentProps} />,
});
}
if (this.shouldShowComputedPropertiesEditor) {
tabs.push({
tab: SettingsV2TabTypes.ComputedPropertiesTab,
content: <ComputedPropertiesComponent {...computedPropertiesComponentProps} />,
});
}
const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange,
selectedKey: SettingsV2TabTypes[this.state.selectedTab],

View File

@@ -11,6 +11,7 @@ import {
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage,
getToolTipContainer,
indexingPolicynUnsavedWarningMessage,
manualToAutoscaleDisclaimerElement,
mongoIndexTransformationRefreshingMessage,
mongoIndexingPolicyAADError,
@@ -38,6 +39,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{manualToAutoscaleDisclaimerElement}
{ttlWarning}
{indexingPolicynUnsavedWarningMessage}
{updateThroughputDelayedApplyWarningMessage}
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}

View File

@@ -61,8 +61,6 @@ export interface PriceBreakdown {
currencySign: string;
}
export type editorType = "indexPolicy" | "computedProperties";
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
@@ -256,10 +254,9 @@ export const ttlWarning: JSX.Element = (
</Text>
);
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
You have not saved the latest changes made to your{" "}
{editor === "indexPolicy" ? "indexing policy" : "computed properties"}. Please click save to confirm the changes.
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
</Text>
);

View File

@@ -1,56 +0,0 @@
import * as DataModels from "Contracts/DataModels";
import { shallow } from "enzyme";
import React from "react";
import { ComputedPropertiesComponent, ComputedPropertiesComponentProps } from "./ComputedPropertiesComponent";
describe("ComputedPropertiesComponent", () => {
const initialComputedPropertiesContent: DataModels.ComputedProperties = [
{
name: "prop1",
query: "query1",
},
];
const baseProps: ComputedPropertiesComponentProps = {
computedPropertiesContent: initialComputedPropertiesContent,
computedPropertiesContentBaseline: initialComputedPropertiesContent,
logComputedPropertiesSuccessMessage: () => {
return;
},
onComputedPropertiesContentChange: () => {
return;
},
onComputedPropertiesDirtyChange: () => {
return;
},
resetShouldDiscardComputedProperties: () => {
return;
},
shouldDiscardComputedProperties: false,
};
it("renders", () => {
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
});
it("computed properties are reset", () => {
const wrapper = shallow(<ComputedPropertiesComponent {...baseProps} />);
const computedPropertiesComponentInstance = wrapper.instance() as ComputedPropertiesComponent;
const resetComputedPropertiesEditorMockFn = jest.fn();
computedPropertiesComponentInstance.resetComputedPropertiesEditor = resetComputedPropertiesEditorMockFn;
wrapper.setProps({ shouldDiscardComputedProperties: true });
wrapper.update();
expect(resetComputedPropertiesEditorMockFn.mock.calls.length).toEqual(1);
});
it("dirty is set", () => {
let computedPropertiesComponent = new ComputedPropertiesComponent(baseProps);
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(false);
const newProps = { ...baseProps, computedPropertiesContent: undefined as DataModels.ComputedProperties };
computedPropertiesComponent = new ComputedPropertiesComponent(newProps);
expect(computedPropertiesComponent.IsComponentDirty()).toEqual(true);
});
});

View File

@@ -1,128 +0,0 @@
import { FontIcon, Link, MessageBar, MessageBarType, Stack, Text } from "@fluentui/react";
import * as DataModels from "Contracts/DataModels";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/Controls/Settings/SettingsRenderUtils";
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { loadMonaco } from "Explorer/LazyMonaco";
import * as monaco from "monaco-editor";
import * as React from "react";
export interface ComputedPropertiesComponentProps {
computedPropertiesContent: DataModels.ComputedProperties;
computedPropertiesContentBaseline: DataModels.ComputedProperties;
logComputedPropertiesSuccessMessage: () => void;
onComputedPropertiesContentChange: (newComputedProperties: DataModels.ComputedProperties) => void;
onComputedPropertiesDirtyChange: (isComputedPropertiesDirty: boolean) => void;
resetShouldDiscardComputedProperties: () => void;
shouldDiscardComputedProperties: boolean;
}
interface ComputedPropertiesComponentState {
computedPropertiesContentIsValid: boolean;
}
export class ComputedPropertiesComponent extends React.Component<
ComputedPropertiesComponentProps,
ComputedPropertiesComponentState
> {
private shouldCheckComponentIsDirty = true;
private computedPropertiesDiv = React.createRef<HTMLDivElement>();
private computedPropertiesEditor: monaco.editor.IStandaloneCodeEditor;
constructor(props: ComputedPropertiesComponentProps) {
super(props);
this.state = {
computedPropertiesContentIsValid: true,
};
}
componentDidUpdate(): void {
if (this.props.shouldDiscardComputedProperties) {
this.resetComputedPropertiesEditor();
this.props.resetShouldDiscardComputedProperties();
}
this.onComponentUpdate();
}
componentDidMount(): void {
this.resetComputedPropertiesEditor();
this.onComponentUpdate();
}
public resetComputedPropertiesEditor = (): void => {
if (!this.computedPropertiesEditor) {
this.createComputedPropertiesEditor();
} else {
const indexingPolicyEditorModel = this.computedPropertiesEditor.getModel();
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
indexingPolicyEditorModel.setValue(value);
}
this.onComponentUpdate();
};
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
this.props.onComputedPropertiesDirtyChange(this.IsComponentDirty());
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): boolean => {
if (
isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) &&
this.state.computedPropertiesContentIsValid
) {
return true;
}
return false;
};
private async createComputedPropertiesEditor(): Promise<void> {
const value: string = JSON.stringify(this.props.computedPropertiesContent, undefined, 4);
const monaco = await loadMonaco();
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
value: value,
language: "json",
ariaLabel: "Computed properties",
});
if (this.computedPropertiesEditor) {
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
computedPropertiesEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
this.props.logComputedPropertiesSuccessMessage();
}
}
private onEditorContentChange = (): void => {
const computedPropertiesEditorModel = this.computedPropertiesEditor.getModel();
try {
const newComputedPropertiesContent = JSON.parse(
computedPropertiesEditorModel.getValue(),
) as DataModels.ComputedProperties;
this.props.onComputedPropertiesContentChange(newComputedPropertiesContent);
this.setState({ computedPropertiesContentIsValid: true });
} catch (e) {
this.setState({ computedPropertiesContentIsValid: false });
}
};
public render(): JSX.Element {
return (
<Stack {...titleAndInputStackProps}>
{isDirty(this.props.computedPropertiesContent, this.props.computedPropertiesContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>
{unsavedEditorWarningMessage("computedProperties")}
</MessageBar>
)}
<Text style={{ marginLeft: "30px", marginBottom: "10px" }}>
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
</Link>
&#160; about how to define computed properties and how to use them.
</Text>
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
</Stack>
);
}
}

View File

@@ -3,7 +3,7 @@ import * as monaco from "monaco-editor";
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
import { isDirty, isIndexTransforming } from "../SettingsUtils";
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
@@ -120,7 +120,7 @@ export class IndexingPolicyComponent extends React.Component<
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/>
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>

View File

@@ -19,6 +19,7 @@ import {
addMongoIndexStackProps,
createAndAddMongoIndexStackProps,
customDetailsListStyles,
indexingPolicynUnsavedWarningMessage,
infoAndToolTipTextStyle,
mediumWidthStackStyles,
mongoCompoundIndexNotSupportedMessage,
@@ -26,16 +27,15 @@ import {
onRenderRow,
separatorStyles,
subComponentStackProps,
unsavedEditorWarningMessage,
} from "../../SettingsRenderUtils";
import {
AddMongoIndexProps,
MongoIndexIdField,
MongoIndexTypes,
MongoNotificationType,
getMongoIndexType,
getMongoIndexTypeText,
isIndexTransforming,
MongoIndexIdField,
MongoIndexTypes,
MongoNotificationType,
} from "../../SettingsUtils";
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
@@ -297,7 +297,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
if (this.getMongoWarningNotificationMessage()) {
warningMessage = this.getMongoWarningNotificationMessage();
} else if (this.isMongoIndexingPolicySaveable()) {
warningMessage = unsavedEditorWarningMessage("indexPolicy");
warningMessage = indexingPolicynUnsavedWarningMessage;
}
return (

View File

@@ -1,216 +0,0 @@
import {
DefaultButton,
FontWeights,
Link,
MessageBar,
MessageBarType,
PrimaryButton,
ProgressIndicator,
Stack,
Text,
} from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { handleError } from "Common/ErrorHandlingUtils";
import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/dataTransfers";
import Explorer from "Explorer/Explorer";
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
import {
CosmosSqlDataTransferDataSourceSink,
DataTransferJobGetResults,
} from "Utils/arm/generatedClients/dataTransferService/types";
import { refreshDataTransferJobs, useDataTransferJobs } from "hooks/useDataTransferJobs";
import { useSidePanel } from "hooks/useSidePanel";
import { userContext } from "../../../../UserContext";
export interface PartitionKeyComponentProps {
database: ViewModels.Database;
collection: ViewModels.Collection;
explorer: Explorer;
}
export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({ database, collection, explorer }) => {
const { dataTransferJobs } = useDataTransferJobs();
const [portalDataTransferJob, setPortalDataTransferJob] = React.useState<DataTransferJobGetResults>(null);
React.useEffect(() => {
const loadDataTransferJobs = refreshDataTransferOperations;
loadDataTransferJobs();
}, []);
React.useEffect(() => {
const currentJob = findPortalDataTransferJob();
setPortalDataTransferJob(currentJob);
startPollingforUpdate(currentJob);
}, [dataTransferJobs]);
const isHierarchicalPartitionedContainer = (): boolean => collection.partitionKey?.kind === "MultiHash";
const getPartitionKeyValue = (): string => {
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
};
const partitionKeyName = "Partition key";
const partitionKeyValue = getPartitionKeyValue();
const textHeadingStyle = {
root: { fontWeight: FontWeights.semibold, fontSize: 16 },
};
const textSubHeadingStyle = {
root: { fontWeight: FontWeights.semibold },
};
const startPollingforUpdate = (currentJob: DataTransferJobGetResults) => {
if (isCurrentJobInProgress(currentJob)) {
const jobName = currentJob?.properties?.jobName;
try {
pollDataTransferJob(
jobName,
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
);
} catch (error) {
handleError(error, "ChangePartitionKey", `Failed to complete data transfer job ${jobName}`);
}
}
};
const cancelRunningDataTransferJob = async (currentJob: DataTransferJobGetResults) => {
await cancelDataTransferJob(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
currentJob?.properties?.jobName,
);
};
const isCurrentJobInProgress = (currentJob: DataTransferJobGetResults) => {
const jobStatus = currentJob?.properties?.status;
return (
jobStatus &&
jobStatus !== "Completed" &&
jobStatus !== "Cancelled" &&
jobStatus !== "Failed" &&
jobStatus !== "Faulted"
);
};
const refreshDataTransferOperations = async () => {
await refreshDataTransferJobs(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
);
};
const findPortalDataTransferJob = (): DataTransferJobGetResults => {
return dataTransferJobs.find((feed: DataTransferJobGetResults) => {
const sourceSink: CosmosSqlDataTransferDataSourceSink = feed?.properties
?.source as CosmosSqlDataTransferDataSourceSink;
return sourceSink.databaseName === collection.databaseId && sourceSink.containerName === collection.id();
});
};
const getProgressDescription = (): string => {
const processedCount = portalDataTransferJob?.properties?.processedCount;
const totalCount = portalDataTransferJob?.properties?.totalCount;
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
};
const startPartitionkeyChangeWorkflow = () => {
useSidePanel
.getState()
.openSidePanel(
"Change partition key",
<ChangePartitionKeyPane
sourceDatabase={database}
sourceCollection={collection}
explorer={explorer}
onClose={refreshDataTransferOperations}
/>,
);
};
const getPercentageComplete = () => {
const jobStatus = portalDataTransferJob?.properties?.status;
const isCompleted = jobStatus === "Completed";
if (isCompleted) {
return 1;
}
const processedCount = portalDataTransferJob?.properties?.processedCount;
const totalCount = portalDataTransferJob?.properties?.totalCount;
const isJobInProgress = isCurrentJobInProgress(portalDataTransferJob);
return isJobInProgress ? (totalCount > 0 ? processedCount / totalCount : null) : 0;
};
return (
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
<Stack tokens={{ childrenGap: 10 }}>
<Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
<Text styles={textSubHeadingStyle}>Partitioning</Text>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text>{partitionKeyValue}</Text>
<Text>{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}</Text>
</Stack>
</Stack>
</Stack>
<MessageBar messageBarType={MessageBarType.warning}>
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the
source container for the entire duration of the partition key change process.
<Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
underline
>
Learn more
</Link>
</MessageBar>
<Text>
To change the partition key, a new destination container must be created or an existing destination container
selected. Data will then be copied to the destination container.
</Text>
<PrimaryButton
styles={{ root: { width: "fit-content" } }}
text="Change"
onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)}
/>
{portalDataTransferJob && (
<Stack>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
<Stack
horizontal
tokens={{ childrenGap: 20 }}
styles={{
root: {
alignItems: "center",
},
}}
>
<ProgressIndicator
label={portalDataTransferJob?.properties?.jobName}
description={getProgressDescription()}
percentComplete={getPercentageComplete()}
styles={{
root: {
width: "85%",
},
}}
></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
)}
</Stack>
</Stack>
)}
</Stack>
);
};

View File

@@ -306,7 +306,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
};
const costElement = (): JSX.Element => {
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
return (
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}

View File

@@ -917,7 +917,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
>
$
0.0080
0.012
/hr
</Text>
<Text
@@ -929,7 +929,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
>
$
0.19
0.29
/day
</Text>
<Text
@@ -941,7 +941,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
>
$
5.84
8.76
/mo
</Text>
</Stack>
@@ -1354,7 +1354,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
>
$
0.0080
0.012
/hr
</Text>
<Text
@@ -1366,7 +1366,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
>
$
0.19
0.29
/day
</Text>
<Text
@@ -1378,7 +1378,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
>
$
5.84
8.76
/mo
</Text>
</Stack>

View File

@@ -1,36 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ComputedPropertiesComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<Text
style={
Object {
"marginBottom": "10px",
"marginLeft": "30px",
}
}
>
<StyledLinkBase
href="https://aka.ms/computed-properties-preview/"
target="_blank"
>
Learn more
<FontIcon
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
  about how to define computed properties and how to use them.
</Text>
<div
className="settingsV2IndexingPolicyEditor"
tabIndex={0}
/>
</Stack>
`;

View File

@@ -4,7 +4,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
const zeroValue = 0;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy | DataModels.ComputedProperties;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
export const TtlOff = "off";
export const TtlOn = "on";
export const TtlOnNoDefault = "on-nodefault";
@@ -45,8 +45,6 @@ export enum SettingsV2TabTypes {
ConflictResolutionTab,
SubSettingsTab,
IndexingPolicyTab,
PartitionKeyTab,
ComputedPropertiesTab,
}
export interface IsComponentDirtyResult {
@@ -148,10 +146,6 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
return "Settings";
case SettingsV2TabTypes.IndexingPolicyTab:
return "Indexing Policy";
case SettingsV2TabTypes.PartitionKeyTab:
return "Partition Keys (preview)";
case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties (preview)";
default:
throw new Error(`Unknown tab ${tab}`);
}
@@ -205,49 +199,3 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => {
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
// index transformation progress can be 0
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
};
export const getPartitionKeyTooltipText = (apiType: string): string => {
if (apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${getPartitionKeyName(
apiType,
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
};
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
return subtext;
}
return "";
};
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
switch (apiType) {
case "Mongo":
return "e.g., categoryId";
case "Gremlin":
return "e.g., /address";
case "SQL":
return `${
index === undefined
? "Required - first partition key e.g., /TenantId"
: index === 0
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return "e.g., /address/zipCode";
}
};

View File

@@ -40,12 +40,6 @@ export const collection = {
version: 2,
},
partitionKeyProperties: ["partitionKey"],
computedProperties: ko.observable<DataModels.ComputedProperties>([
{
name: "queryName",
query: "query",
},
]),
readSettings: () => {
return;
},

View File

@@ -26,7 +26,6 @@ exports[`SettingsComponent renders 1`] = `
Object {
"analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
@@ -104,7 +103,6 @@ exports[`SettingsComponent renders 1`] = `
Object {
"analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
@@ -206,133 +204,6 @@ exports[`SettingsComponent renders 1`] = `
shouldDiscardIndexingPolicy={false}
/>
</PivotItem>
<PivotItem
headerText="Partition Keys (preview)"
itemKey="PartitionKeyTab"
key="PartitionKeyTab"
style={
Object {
"marginTop": 20,
}
}
>
<PartitionKeyComponent
collection={
Object {
"analyticalStorageTtl": [Function],
"changeFeedPolicy": [Function],
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
"databaseId": "test",
"defaultTtl": [Function],
"geospatialConfig": [Function],
"getDatabase": [Function],
"id": [Function],
"indexingPolicy": [Function],
"offer": [Function],
"partitionKey": Object {
"kind": "hash",
"paths": Array [],
"version": 2,
},
"partitionKeyProperties": Array [
"partitionKey",
],
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
}
}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}
}
/>
</PivotItem>
<PivotItem
headerText="Computed Properties (preview)"
itemKey="ComputedPropertiesTab"
key="ComputedPropertiesTab"
style={
Object {
"marginTop": 20,
}
}
>
<ComputedPropertiesComponent
computedPropertiesContent={
Array [
Object {
"name": "queryName",
"query": "query",
},
]
}
computedPropertiesContentBaseline={
Array [
Object {
"name": "queryName",
"query": "query",
},
]
}
logComputedPropertiesSuccessMessage={[Function]}
onComputedPropertiesContentChange={[Function]}
onComputedPropertiesDirtyChange={[Function]}
resetShouldDiscardComputedProperties={[Function]}
shouldDiscardComputedProperties={false}
/>
</PivotItem>
</StyledPivot>
</div>
</div>

View File

@@ -99,6 +99,18 @@ exports[`SettingsUtils functions render 1`] = `
</StyledLinkBase>
.
</Text>
<Text
styles={
Object {
"root": Object {
"color": "windowtext",
"fontSize": 14,
},
}
}
>
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
</Text>
<Text
id="updateThroughputDelayedApplyWarningMessage"
styles={

View File

@@ -14,13 +14,7 @@
.throughputInputSpacing > :not(:last-child) {
margin-bottom: @DefaultSpace;
}
.capacitycalculator-link:focus {
.capacitycalculator-link:focus{
text-decoration: underline;
outline-offset: 2px;
}
.copyQuery:focus::after,
.deleteQuery:focus::after {
outline: none !important;
}
}

View File

@@ -247,7 +247,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
name="More"
title="More"
className="treeMenuEllipsis"
ariaLabel={`${menuItemLabel} options`}
ariaLabel={menuItemLabel}
menuIconProps={{
iconName: menuItemLabel,
styles: { root: { fontSize: "18px", fontWeight: "bold" } },

View File

@@ -172,7 +172,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
onKeyPress={[Function]}
>
<CustomizedIconButton
ariaLabel="More options"
ariaLabel="More"
className="treeMenuEllipsis"
menuIconProps={
Object {
@@ -397,7 +397,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
onKeyPress={[Function]}
>
<CustomizedIconButton
ariaLabel="More options"
ariaLabel="More"
className="treeMenuEllipsis"
menuIconProps={
Object {

View File

@@ -296,14 +296,6 @@ export default class Explorer {
}
}
public async openCESCVAFeedbackBlade(): Promise<void> {
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
Logger.logInfo(
`CES CVA Feedback logging current date when survey is shown ${Date.now().toString()}`,
"Explorer/openCESCVAFeedbackBlade",
);
}
public async refreshDatabaseForResourceToken(): Promise<void> {
const databaseId = userContext.parsedResourceToken?.databaseId;
const collectionId = userContext.parsedResourceToken?.collectionId;

View File

@@ -1,4 +1,3 @@
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
@@ -9,7 +8,6 @@ import AddUdfIcon from "../../../../images/AddUdf.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import HomeIcon from "../../../../images/Home_16.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import GitHubIcon from "../../../../images/github.svg";
@@ -23,7 +21,7 @@ import * as Constants from "../../../Common/Constants";
import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { JunoClient } from "../../../Juno/JunoClient";
import { updateUserContext, userContext } from "../../../UserContext";
import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
@@ -59,9 +57,6 @@ export function createStaticCommandBarButtons(
};
if (configContext.platform !== Platform.Fabric) {
const homeBtn = createHomeButton();
buttons.push(homeBtn);
const newCollectionBtn = createNewCollectionGroup(container);
buttons.push(newCollectionBtn);
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
@@ -168,14 +163,6 @@ export function createStaticCommandBarButtons(
}
}
// Attempting to add region selection button here.
addDivider();
const [selectedRegion, setSelectedRegion] = React.useState(
userContext.databaseAccount.properties.locations[0].locationName,
);
const newReadRegionSelectionBtn = createReadRegionSelectionGroup(container, selectedRegion, setSelectedRegion);
buttons.push(newReadRegionSelectionBtn);
return buttons;
}
@@ -209,22 +196,18 @@ export function createContextCommandBarButtons(
}
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] =
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
? []
: [
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: () =>
useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",
hasPopup: true,
disabled: false,
},
];
const buttons: CommandButtonComponentProps[] = [
{
iconSrc: SettingsIcon,
iconAlt: "Settings",
onCommandClick: () => useSidePanel.getState().openSidePanel("Settings", <SettingsPane explorer={container} />),
commandButtonLabel: undefined,
ariaLabel: "Settings",
tooltipText: "Settings",
hasPopup: true,
disabled: false,
},
];
const showOpenFullScreen =
configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin";
@@ -248,12 +231,12 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
buttons.push(fullScreenButton);
}
if (configContext.platform === Platform.Portal) {
if (configContext.platform !== Platform.Emulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: () => container.openCESCVAFeedbackBlade(),
onCommandClick: () => container.provideFeedbackEmail(),
commandButtonLabel: undefined,
ariaLabel: label,
tooltipText: label,
@@ -298,18 +281,6 @@ function createNewCollectionGroup(container: Explorer): CommandButtonComponentPr
};
}
function createHomeButton(): CommandButtonComponentProps {
const label = "Home";
return {
iconSrc: HomeIcon,
iconAlt: label,
onCommandClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Home),
commandButtonLabel: label,
hasPopup: false,
ariaLabel: label,
};
}
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
@@ -609,103 +580,6 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
};
}
// function createReadRegionSelectionGroup(container: Explorer): CommandButtonComponentProps {
// const label = "Select a Read Region";
// return {
// iconAlt: label,
// commandButtonLabel: label,
// hasPopup: false,
// disabled: false,
// ariaLabel: label,
// onCommandClick: async () => {
// console.log(
// `CURRENT DOCUMENT ENDPOINT: ${JSON.stringify(userContext.databaseAccount.properties.documentEndpoint)}`,
// );
// userContext.databaseAccount.properties.readLocations.forEach((readLocation) => {
// console.log(`CURRENT READ ENDPOINT(S): ${JSON.stringify(readLocation)}`);
// });
// const updatedDatabaseAccount = {
// ...userContext.databaseAccount,
// properties: {
// ...userContext.databaseAccount.properties,
// documentEndpoint: "https://test-craig-nosql-periodic-eastus.documents.azure.com:443/",
// },
// };
// updateUserContext({
// databaseAccount: updatedDatabaseAccount,
// });
// },
// };
// }
function createReadRegionSelectionGroup(
container: Explorer,
selectedRegion: string,
setSelectedRegion: (region: string) => void,
): CommandButtonComponentProps {
const children = userContext.databaseAccount.properties.readLocations.map((readLocation) => ({
commandButtonLabel: readLocation.locationName,
onCommandClick: async () => {
const updatedDatabaseAccount = {
...userContext.databaseAccount,
properties: {
...userContext.databaseAccount.properties,
documentEndpoint: readLocation.documentEndpoint,
},
};
updateUserContext({
databaseAccount: updatedDatabaseAccount,
});
setSelectedRegion(readLocation.locationName);
},
hasPopup: false,
ariaLabel: `Select ${readLocation.locationName}`,
}));
const label = selectedRegion || "Select a Read Region";
return {
iconAlt: label,
commandButtonLabel: label,
onCommandClick: () => {},
hasPopup: true,
ariaLabel: label,
isDropdown: true,
children,
dropdownWidth: 100,
};
}
// function createAccountRegionSelectionButton(container: Explorer): JSX.Element {
// const [selectedEndpoint, setSelectedEndpoint] = useState(userContext.databaseAccount.properties.documentEndpoint);
// const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
// const endpoint = event.target.value as string;
// setSelectedEndpoint(endpoint);
// const updatedDatabaseAccount = {
// ...userContext.databaseAccount,
// properties: {
// ...userContext.databaseAccount.properties,
// documentEndpoint: endpoint,
// },
// };
// updateUserContext({
// databaseAccount: updatedDatabaseAccount,
// });
// };
// return (
// <Select
// value={selectedEndpoint}
// onChange={handleChange}
// displayEmpty
// >
// {userContext.databaseAccount.properties.readLocations.map((readLocation) => (
// <MenuItem value={readLocation}>{readLocation}</MenuItem>
// ))}
// </Select>
// );
// }
function createStaticCommandBarButtonsForResourceToken(
container: Explorer,
selectedNodeState: SelectedNodeState,

View File

@@ -37,7 +37,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
if (isDisabled) {
return StyleConstants.GrayScale;
}
return configContext.platform == Platform.Fabric ? StyleConstants.FabricToolbarIconColor : undefined;
return configContext.platform == Platform.Fabric ? StyleConstants.NoColor : undefined;
};
return btns
@@ -96,12 +96,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
},
width: 16,
},
label: {
fontSize:
configContext.platform == Platform.Fabric
? StyleConstants.DefaultFontSize
: StyleConstants.mediumFontSize,
},
label: { fontSize: StyleConstants.mediumFontSize },
rootHovered: { backgroundColor: hoverColor },
rootPressed: { backgroundColor: hoverColor },
splitButtonMenuButtonExpanded: {
@@ -138,12 +133,7 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
// TODO Figure out how to do it the proper way with subComponentStyles.
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
selectors: {
".ms-ContextualMenu-itemText": {
fontSize:
configContext.platform == Platform.Fabric
? StyleConstants.DefaultFontSize
: StyleConstants.mediumFontSize,
},
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
".ms-ContextualMenu-link:hover": { backgroundColor: hoverColor },
".ms-ContextualMenu-icon": { width: 16, height: 16 },
},

View File

@@ -162,7 +162,6 @@ export class NotificationConsoleComponent extends React.Component<
role="button"
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
tabIndex={0}
style={{ border: "1px solid black", borderRadius: "2px" }}
>
<img src={ClearIcon} alt="clear notifications image" />
Clear Notifications

View File

@@ -146,12 +146,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
onClick={[Function]}
onKeyDown={[Function]}
role="button"
style={
Object {
"border": "1px solid black",
"borderRadius": "2px",
}
}
tabIndex={0}
>
<img
@@ -317,12 +311,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
onClick={[Function]}
onKeyDown={[Function]}
role="button"
style={
Object {
"border": "1px solid black",
"borderRadius": "2px",
}
}
tabIndex={0}
>
<img

View File

@@ -1,5 +1,4 @@
// TODO convert this file to an action registry in order to have actions and their handlers be more tightly coupled.
import { useDatabases } from "Explorer/useDatabases";
import React from "react";
import { ActionContracts } from "../../Contracts/ExplorerContracts";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -41,112 +40,97 @@ function openCollectionTab(
databases: ViewModels.Database[],
initialDatabaseIndex = 0,
) {
//if databases are not yet loaded, wait until loaded
if (!databases || databases.length === 0) {
const databaseActionHandler = (databases: ViewModels.Database[]) => {
databasesUnsubscription();
openCollectionTab(action, databases, 0);
return;
};
const databasesUnsubscription = useDatabases.subscribe(databaseActionHandler, (state) => state.databases);
} else {
for (let i = initialDatabaseIndex; i < databases.length; i++) {
const database: ViewModels.Database = databases[i];
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
continue;
}
//expand database first if not expanded to load the collections
if (!database.isDatabaseExpanded?.()) {
database.expandDatabase?.();
}
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
if (!action.collectionResourceId && collections.length === 0) {
subscription.dispose();
openCollectionTab(action, databases, ++i);
return;
}
for (let j = 0; j < collections.length; j++) {
const collection: ViewModels.Collection = collections[j];
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
continue;
}
// select the collection
collection.expandCollection();
if (
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
) {
collection.onDocumentDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
) {
collection.onMongoDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.TableEntities ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
) {
collection.onTableEntitiesClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.Graph ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
) {
collection.onGraphDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SQLQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) {
collection.onNewQueryClick(
collection,
undefined,
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
);
break;
}
if (
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
) {
collection.onSettingsClick();
break;
}
}
subscription.dispose();
};
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections));
if (database.collections && database.collections() && database.collections().length) {
collectionActionHandler(database.collections());
}
break;
for (let i = initialDatabaseIndex; i < databases.length; i++) {
const database: ViewModels.Database = databases[i];
if (!!action.databaseResourceId && database.id() !== action.databaseResourceId) {
continue;
}
const collectionActionHandler = (collections: ViewModels.Collection[]) => {
if (!action.collectionResourceId && collections.length === 0) {
subscription.dispose();
openCollectionTab(action, databases, ++i);
return;
}
for (let j = 0; j < collections.length; j++) {
const collection: ViewModels.Collection = collections[j];
if (!!action.collectionResourceId && collection.id() !== action.collectionResourceId) {
continue;
}
// select the collection
collection.expandCollection();
if (
action.tabKind === ActionContracts.TabKind.SQLDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLDocuments]
) {
collection.onDocumentDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.MongoDocuments ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.MongoDocuments]
) {
collection.onMongoDBDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.TableEntities ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
) {
collection.onTableEntitiesClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.Graph ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.Graph]
) {
collection.onGraphDocumentsClick();
break;
}
if (
action.tabKind === ActionContracts.TabKind.SQLQuery ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SQLQuery]
) {
collection.onNewQueryClick(
collection,
undefined,
generateQueryText(action as ActionContracts.OpenQueryTab, collection.partitionKeyProperties),
);
break;
}
if (
action.tabKind === ActionContracts.TabKind.ScaleSettings ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.ScaleSettings]
) {
collection.onSettingsClick();
break;
}
}
subscription.dispose();
};
const subscription = database.collections.subscribe((collections) => collectionActionHandler(collections));
if (database.collections && database.collections() && database.collections().length) {
collectionActionHandler(database.collections());
}
break;
}
}

View File

@@ -1,398 +0,0 @@
import {
DefaultButton,
DirectionalHint,
Dropdown,
IDropdownOption,
Icon,
IconButton,
Link,
MessageBar,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { createCollection } from "Common/dataAccess/createCollection";
import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers";
import * as DataModels from "Contracts/DataModels";
import * as ViewModels from "Contracts/ViewModels";
import {
getPartitionKeyName,
getPartitionKeyPlaceHolder,
getPartitionKeySubtext,
getPartitionKeyTooltipText,
} from "Explorer/Controls/Settings/SettingsUtils";
import Explorer from "Explorer/Explorer";
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
import { useDatabases } from "Explorer/useDatabases";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { useSidePanel } from "hooks/useSidePanel";
import * as React from "react";
export interface ChangePartitionKeyPaneProps {
sourceDatabase: ViewModels.Database;
sourceCollection: ViewModels.Collection;
explorer: Explorer;
onClose: () => Promise<void>;
}
export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
sourceDatabase,
sourceCollection,
explorer,
onClose,
}) => {
const [targetCollectionId, setTargetCollectionId] = React.useState<string>();
const [createNewContainer, setCreateNewContainer] = React.useState<boolean>(true);
const [formError, setFormError] = React.useState<string>();
const [isExecuting, setIsExecuting] = React.useState<boolean>(false);
const [subPartitionKeys, setSubPartitionKeys] = React.useState<string[]>([]);
const [partitionKey, setPartitionKey] = React.useState<string>();
const getCollectionOptions = (): IDropdownOption[] => {
return sourceDatabase
.collections()
.filter((collection) => collection.id !== sourceCollection.id)
.map((collection) => ({
key: collection.id(),
text: collection.id(),
}));
};
const submit = async () => {
if (!validateInputs()) {
return;
}
setIsExecuting(true);
try {
createNewContainer && (await createContainer());
await createDataTransferJob();
await onClose();
} catch (error) {
handleError(error, "ChangePartitionKey", "Failed to start data transfer job");
}
setIsExecuting(false);
useSidePanel.getState().closeSidePanel();
};
const validateInputs = (): boolean => {
if (!createNewContainer && !targetCollectionId) {
setFormError("Choose an existing container");
return false;
}
return true;
};
const createDataTransferJob = async () => {
const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`;
const dataTransferParams: DataTransferParams = {
jobName,
apiType: userContext.apiType,
subscriptionId: userContext.subscriptionId,
resourceGroupName: userContext.resourceGroup,
accountName: userContext.databaseAccount.name,
sourceDatabaseName: sourceDatabase.id(),
sourceCollectionName: sourceCollection.id(),
targetDatabaseName: sourceDatabase.id(),
targetCollectionName: targetCollectionId,
};
await initiateDataTransfer(dataTransferParams);
};
const createContainer = async () => {
const partitionKeyString = partitionKey.trim();
const partitionKeyData: DataModels.PartitionKey = partitionKeyString
? {
paths: [partitionKeyString, ...(subPartitionKeys.length > 0 ? subPartitionKeys : [])],
kind: subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
version: 2,
}
: undefined;
const createCollectionParams: DataModels.CreateCollectionParams = {
createNewDatabase: false,
collectionId: targetCollectionId,
databaseId: sourceDatabase.id(),
databaseLevelThroughput: isSelectedDatabaseSharedThroughput(),
offerThroughput: sourceCollection.offer()?.manualThroughput,
autoPilotMaxThroughput: sourceCollection.offer()?.autoscaleMaxThroughput,
partitionKey: partitionKeyData,
};
await createCollection(createCollectionParams);
await explorer.refreshAllDatabases();
};
const isSelectedDatabaseSharedThroughput = (): boolean => {
const selectedDatabase = useDatabases
.getState()
.databases?.find((database) => database.id() === sourceDatabase.id());
return !!selectedDatabase?.offer();
};
return (
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK">
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
<Text variant="small">
When changing a containers partition key, you will need to create a destination container with the correct
partition key. You may also select an existing destination container.&nbsp;
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
target="_blank"
underline
>
Learn more
</Link>
</Text>
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Database id
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
aria-label={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
<Dropdown
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
options={[]}
placeholder={sourceDatabase.id()}
responsiveMode={999}
disabled={true}
/>
</Stack>
<Stack className="panelGroupSpacing" horizontal verticalAlign="center">
<div role="radiogroup">
<input
className="panelRadioBtn"
checked={createNewContainer}
aria-label="Create new container"
aria-checked={createNewContainer}
name="containerType"
type="radio"
role="radio"
id="containerCreateNew"
tabIndex={0}
onChange={() => setCreateNewContainer(true)}
/>
<span className="panelRadioBtnLabel">New container</span>
<input
className="panelRadioBtn"
checked={!createNewContainer}
aria-label="Use existing container"
aria-checked={!createNewContainer}
name="containerType"
type="radio"
role="radio"
tabIndex={0}
onChange={() => setCreateNewContainer(false)}
/>
<span className="panelRadioBtnLabel">Existing container</span>
</div>
</Stack>
{createNewContainer ? (
<Stack>
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
<Stack className="panelGroupSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{`${getCollectionName()} id`}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
<input
name="collectionId"
id="collectionId"
type="text"
aria-required
required
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={targetCollectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
/>
</Stack>
<Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{getPartitionKeyName(userContext.apiType)}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={getPartitionKeyTooltipText(userContext.apiType)}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
aria-label={getPartitionKeyTooltipText(userContext.apiType)}
/>
</TooltipHost>
</Stack>
<Text variant="small" aria-label="pkDescription">
{getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)}
</Text>
<input
type="text"
id="addCollection-partitionKeyValue"
aria-required
required
size={40}
className="panelTextField"
placeholder={getPartitionKeyPlaceHolder(userContext.apiType)}
aria-label={getPartitionKeyName(userContext.apiType)}
pattern={".*"}
title={""}
value={partitionKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (!partitionKey && !event.target.value.startsWith("/")) {
setPartitionKey("/" + event.target.value);
} else {
setPartitionKey(event.target.value);
}
}}
/>
{subPartitionKeys.map((subPartitionKey: string, index: number) => {
return (
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
<div
style={{
width: "20px",
border: "solid",
borderWidth: "0px 0px 1px 1px",
marginRight: "5px",
}}
></div>
<input
type="text"
id="addCollection-partitionKeyValue"
key={`addCollection-partitionKeyValue_${index}`}
aria-required
required
size={40}
tabIndex={index > 0 ? 1 : 0}
className="panelTextField"
autoComplete="off"
placeholder={getPartitionKeyPlaceHolder(userContext.apiType, index)}
aria-label={getPartitionKeyName(userContext.apiType)}
pattern={".*"}
title={""}
value={subPartitionKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const keys = [...subPartitionKeys];
if (!keys[index] && !event.target.value.startsWith("/")) {
keys[index] = "/" + event.target.value.trim();
setSubPartitionKeys(keys);
} else {
keys[index] = event.target.value.trim();
setSubPartitionKeys(keys);
}
}}
/>
<IconButton
iconProps={{ iconName: "Delete" }}
style={{ height: 27 }}
onClick={() => {
const keys = subPartitionKeys.filter((uniqueKey, j) => index !== j);
setSubPartitionKeys(keys);
}}
/>
</Stack>
);
})}
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
>
Add hierarchical partition key
</DefaultButton>
{subPartitionKeys.length > 0 && (
<Text variant="small">
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
Learn more
</Link>
</Text>
)}
</Stack>
</Stack>
</Stack>
) : (
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{`${getCollectionName()}`}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
<Dropdown
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing container"
options={getCollectionOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, collection: IDropdownOption) => {
setTargetCollectionId(collection.key as string);
setFormError("");
}}
defaultSelectedKey={targetCollectionId}
responsiveMode={999}
/>
</Stack>
)}
</Stack>
</RightPaneForm>
);
};

View File

@@ -58,7 +58,7 @@ export class PanelContainerComponent extends React.Component<PanelContainerProps
onDismiss={this.onDissmiss}
isLightDismiss
type={PanelType.custom}
closeButtonAriaLabel={`Close ${this.props.headerText}`}
closeButtonAriaLabel="Close"
customWidth={this.props.panelWidth ? this.props.panelWidth : "440px"}
headerClassName="panelHeader"
onRenderNavigationContent={this.props.onRenderNavigationContent}

View File

@@ -486,4 +486,4 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
isButtonDisabled={false}
/>
</form>
`;
`;

View File

@@ -2,7 +2,7 @@
exports[`PaneContainerComponent test should be resize if notification console is expanded 1`] = `
<StyledPanelBase
closeButtonAriaLabel="Close test"
closeButtonAriaLabel="Close"
customWidth="440px"
headerClassName="panelHeader"
headerText="test"
@@ -42,7 +42,7 @@ exports[`PaneContainerComponent test should render nothing if content is undefin
exports[`PaneContainerComponent test should render with panel content and header 1`] = `
<StyledPanelBase
closeButtonAriaLabel="Close test"
closeButtonAriaLabel="Close"
customWidth="440px"
headerClassName="panelHeader"
headerText="test"

View File

@@ -49,7 +49,7 @@ export const QueryCopilotFeedbackModal = ({
};
return (
<Modal isOpen={showFeedbackModal} styles={{ main: { borderRadius: 8, maxWidth: 600 } }}>
<Modal isOpen={showFeedbackModal}>
<form onSubmit={handleSubmit}>
<Stack style={{ padding: 24 }}>
<Stack horizontal horizontalAlign="space-between">
@@ -68,14 +68,9 @@ export const QueryCopilotFeedbackModal = ({
rows={3}
/>
<TextField
styles={{
root: { marginBottom: 14 },
fieldGroup: { backgroundColor: "#F3F2F1", borderRadius: 4, borderColor: "#D1D1D1" },
}}
styles={{ root: { marginBottom: 14 } }}
label="Query generated"
defaultValue={generatedQuery}
multiline
rows={3}
readOnly
/>
<Text style={{ fontSize: 12, marginBottom: 14 }}>

View File

@@ -3,14 +3,6 @@
exports[`Query Copilot Feedback Modal snapshot test shoud render and match snapshot 1`] = `
<Modal
isOpen={true}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -75,16 +67,9 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -140,14 +125,6 @@ exports[`Query Copilot Feedback Modal snapshot test shoud render and match snaps
exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -212,16 +189,9 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -277,14 +247,6 @@ exports[`Query Copilot Feedback Modal snapshot test should cancel submission 1`]
exports[`Query Copilot Feedback Modal snapshot test should close on cancel click 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -349,16 +311,9 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -414,14 +369,6 @@ exports[`Query Copilot Feedback Modal snapshot test should close on cancel click
exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -486,16 +433,9 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -551,14 +491,6 @@ exports[`Query Copilot Feedback Modal snapshot test should get user unput 1`] =
exports[`Query Copilot Feedback Modal snapshot test should not render dont show again button 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -623,16 +555,9 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -688,14 +613,6 @@ exports[`Query Copilot Feedback Modal snapshot test should not render dont show
exports[`Query Copilot Feedback Modal snapshot test should render dont show again button and check it 1`] = `
<Modal
isOpen={true}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -760,16 +677,9 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},
@@ -840,14 +750,6 @@ exports[`Query Copilot Feedback Modal snapshot test should render dont show agai
exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`] = `
<Modal
isOpen={false}
styles={
Object {
"main": Object {
"borderRadius": 8,
"maxWidth": 600,
},
}
}
>
<form
onSubmit={[Function]}
@@ -912,16 +814,9 @@ exports[`Query Copilot Feedback Modal snapshot test should submit submission 1`]
<StyledTextFieldBase
defaultValue="test query"
label="Query generated"
multiline={true}
readOnly={true}
rows={3}
styles={
Object {
"fieldGroup": Object {
"backgroundColor": "#F3F2F1",
"borderColor": "#D1D1D1",
"borderRadius": 4,
},
"root": Object {
"marginBottom": 14,
},

View File

@@ -11,8 +11,8 @@ import {
Link,
MessageBar,
MessageBarType,
ProgressIndicator,
Separator,
Spinner,
Stack,
TeachingBubble,
Text,
@@ -21,6 +21,7 @@ import {
import { HttpStatusCodes } from "Common/Constants";
import { handleError } from "Common/ErrorHandlingUtils";
import { createUri } from "Common/UrlUtility";
import { WelcomeModal } from "Explorer/QueryCopilot/Modal/WelcomeModal";
import { CopyPopup } from "Explorer/QueryCopilot/Popup/CopyPopup";
import { DeletePopup } from "Explorer/QueryCopilot/Popup/DeletePopup";
import {
@@ -36,6 +37,7 @@ import { userContext } from "UserContext";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import React, { useRef, useState } from "react";
import HintIcon from "../../../images/Hint.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import RecentIcon from "../../../images/Recent.svg";
import errorIcon from "../../../images/close-black.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -214,12 +216,12 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
if (response.ok) {
if (generateSQLQueryResponse?.sql !== "N/A") {
const queryExplanation = `-- **Explanation of query:** ${
generateSQLQueryResponse.explanation ? generateSQLQueryResponse.explanation : "N/A"
}\r\n`;
const currentGeneratedQuery = queryExplanation + generateSQLQueryResponse.sql;
const lastQuery = generatedQuery && query ? `${query}\r\n` : "";
setQuery(`${lastQuery}${currentGeneratedQuery}`);
let query = `-- **Prompt:** ${userPrompt}\r\n`;
if (generateSQLQueryResponse.explanation) {
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
}
query += generateSQLQueryResponse.sql;
setQuery(query);
setGeneratedQuery(generateSQLQueryResponse.sql);
setGeneratedQueryComments(generateSQLQueryResponse.explanation);
setShowFeedbackBar(true);
@@ -270,11 +272,28 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
}
};
const showTeachingBubble = (): void => {
if (showPromptTeachingBubble && !inputEdited.current) {
setTimeout(() => {
if (!inputEdited.current && !isWelcomModalVisible()) {
setCopilotTeachingBubbleVisible(true);
inputEdited.current = true;
}
}, 30000);
} else {
toggleCopilotTeachingBubbleVisible(false);
}
};
const toggleCopilotTeachingBubbleVisible = (visible: boolean): void => {
setCopilotTeachingBubbleVisible(visible);
setShowPromptTeachingBubble(visible);
};
const isWelcomModalVisible = (): boolean => {
return localStorage.getItem("hideWelcomeModal") !== "true";
};
const clearFeedback = () => {
resetButtonState();
resetQueryResults();
@@ -303,394 +322,19 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
};
React.useEffect(() => {
showTeachingBubble();
useTabs.getState().setIsQueryErrorThrown(false);
}, []);
return (
<Stack
className="copilot-prompt-pane"
styles={{ root: { backgroundColor: "#FAFAFA", padding: "8px" } }}
styles={{ root: { backgroundColor: "#FAFAFA", padding: "16px 24px 0px" } }}
id="copilot-textfield-label"
>
<Stack
horizontal
styles={{
root: {
width: "100%",
borderWidth: 1,
borderStyle: "solid",
borderColor: "#D1D1D1",
borderRadius: 8,
boxShadow: "0px 4px 8px 0px #00000024",
},
}}
>
<Stack style={{ width: "100%" }}>
<Stack horizontal verticalAlign="center" style={{ padding: "8px 8px 0px 8px" }}>
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={handleUserPromptChange}
onClick={() => {
inputEdited.current = true;
setShowSamplePrompts(true);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && userPrompt) {
inputEdited.current = true;
startGenerateQueryProcess();
}
}}
style={{ lineHeight: 30 }}
styles={{
root: { width: "100%" },
suffix: {
background: "none",
padding: 0,
},
fieldGroup: {
borderRadius: 4,
borderColor: "#D1D1D1",
"::after": {
border: "inherit",
borderWidth: 2,
borderBottomColor: "#464FEB",
borderRadius: 4,
},
},
}}
disabled={isGeneratingQuery}
autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label"
onRenderSuffix={() => {
return (
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ background: "none" }}
onClick={() => startGenerateQueryProcess()}
aria-label="Send"
/>
);
}}
/>
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
<TeachingBubble
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
target="#naturalLanguageInput"
hasCloseButton={true}
closeButtonAriaLabel="Close"
onDismiss={() => toggleCopilotTeachingBubbleVisible(false)}
hasSmallHeadline={true}
headline="Write a prompt"
>
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);
toggleCopilotTeachingBubbleVisible(false);
}}
style={{ color: "white", fontWeight: 600 }}
>
sample prompts
</Link>{" "}
or write your own query
</TeachingBubble>
)}
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
target="#naturalLanguageInput"
isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)}
directionalHintFixed={true}
directionalHint={DirectionalHint.bottomLeftEdge}
alignTargetEdge={true}
gapSpace={4}
>
<Stack>
{filteredHistories?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Recent
</Text>
{filteredHistories.map((history, i) => (
<DefaultButton
key={i}
onClick={() => {
setUserPrompt(history);
setShowSamplePrompts(false);
inputEdited.current = true;
}}
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
styles={promptStyles}
>
{history}
</DefaultButton>
))}
</Stack>
)}
{filteredSuggestedPrompts?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Suggested Prompts
</Text>
{filteredSuggestedPrompts.map((prompt) => (
<DefaultButton
key={prompt.id}
onClick={() => {
setUserPrompt(prompt.text);
setShowSamplePrompts(false);
inputEdited.current = true;
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
{prompt.text}
</DefaultButton>
))}
</Stack>
)}
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
<Stack>
<Separator
styles={{
root: {
selectors: { "::before": { background: "#E1DFDD" } },
padding: 0,
},
}}
/>
<Text
style={{
width: "100%",
fontSize: 14,
marginLeft: 16,
padding: "4px 0",
}}
>
Learn about{" "}
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
writing effective prompts
</Link>
</Text>
</Stack>
)}
</Stack>
</Callout>
)}
</Stack>
{!isGeneratingQuery && (
<Stack style={{ padding: 8 }}>
{!showFeedbackBar && (
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072D4" }}>
Read preview terms
</Link>
{showErrorMessageBar && (
<MessageBar messageBarType={MessageBarType.error}>
{errorMessage ? errorMessage : "We ran into an error and were not able to execute query."}
</MessageBar>
)}
{showInvalidQueryMessageBar && (
<MessageBar
messageBarType={MessageBarType.info}
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
>
We were unable to generate a query based upon the prompt provided. Please modify the prompt and
try again. For examples of how to write a good prompt, please read
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
this article.
</Link>{" "}
Our content guidelines can be found
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
here.
</Link>
</MessageBar>
)}
</Text>
)}
{showFeedbackBar && (
<Stack horizontal verticalAlign="center" style={{ maxHeight: 20 }}>
{userContext.feedbackPolicies?.policyAllowFeedback && (
<Stack horizontal verticalAlign="center">
<Text style={{ fontSize: 12 }}>Provide feedback</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
role="status"
style={{ padding: "6px 12px" }}
styles={{
root: {
borderRadius: 8,
},
beakCurtain: {
borderRadius: 8,
},
calloutMain: {
borderRadius: 8,
},
}}
target="#likeBtn"
onDismiss={() => {
setShowCallout(false);
SubmitFeedback({
params: {
generatedQuery: generatedQuery,
likeQuery: likeQuery,
description: "",
userPrompt: userPrompt,
},
explorer,
databaseId,
containerId,
mode: isSampleCopilotActive ? "Sample" : "User",
});
}}
directionalHint={DirectionalHint.topCenter}
>
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setShowCallout(false);
openFeedbackModal(generatedQuery, true, userPrompt);
}}
>
more feedback?
</Link>
</Text>
</Callout>
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 10 }}
aria-label="Like"
role="toggle"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setShowCallout(!likeQuery);
setLikeQuery(!likeQuery);
if (likeQuery === true) {
document.getElementById("likeStatus").innerHTML = "Unpressed";
}
if (likeQuery === false) {
document.getElementById("likeStatus").innerHTML = "Liked";
}
if (dislikeQuery) {
setDislikeQuery(!dislikeQuery);
}
}}
/>
<IconButton
style={{ margin: "0 4px" }}
role="toggle"
aria-label="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => {
let toggleStatusValue = "Unpressed";
if (!dislikeQuery) {
openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false);
toggleStatusValue = "Disliked";
}
setDislikeQuery(!dislikeQuery);
setShowCallout(false);
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
}}
/>
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
<Separator
vertical
styles={{
root: {
"::after": {
backgroundColor: "#767676",
},
},
}}
/>
</Stack>
)}
<CommandBarButton
className="copyQuery"
onClick={copyGeneratedCode}
iconProps={{ iconName: "Copy" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
styles={{
root: {
backgroundColor: "inherit",
},
}}
>
Copy code
</CommandBarButton>
<CommandBarButton
className="deleteQuery"
onClick={() => {
setShowDeletePopup(true);
}}
iconProps={{ iconName: "Delete" }}
style={{ fontSize: 12, transition: "background-color 0.3s ease", height: "100%" }}
styles={{
root: {
backgroundColor: "inherit",
},
}}
>
Clear editor
</CommandBarButton>
</Stack>
)}
</Stack>
)}
{isGeneratingQuery && (
<ProgressIndicator
label="Thinking..."
ariaLabel={getAriaLabel()}
barHeight={4}
styles={{
root: {
fontSize: 12,
width: "100%",
bottom: 0,
},
itemName: {
padding: "0px 8px",
},
itemProgress: {
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
padding: 0,
},
progressBar: {
backgroundImage:
"linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgb(24, 90, 189) 35%, rgb(71, 207, 250) 70%, rgb(180, 124, 248) 92%, rgba(0, 0, 0, 0))",
animationDuration: "5s",
},
}}
/>
)}
</Stack>
<Stack horizontal>
<Image src={CopilotIcon} style={{ width: 24, height: 24 }} alt="Copilot" role="none" />
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
<IconButton
iconProps={{ imageProps: { src: errorIcon } }}
onClick={() => {
@@ -698,10 +342,300 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
clearFeedback();
resetMessageStates();
}}
styles={{
root: {
marginLeft: "auto !important",
},
}}
ariaLabel="Close"
title="Close copilot"
/>
</Stack>
<Stack horizontal verticalAlign="center">
<TextField
id="naturalLanguageInput"
value={userPrompt}
onChange={handleUserPromptChange}
onClick={() => {
inputEdited.current = true;
setShowSamplePrompts(true);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && userPrompt) {
inputEdited.current = true;
startGenerateQueryProcess();
}
}}
style={{ lineHeight: 30 }}
styles={{ root: { width: "95%" }, fieldGroup: { borderRadius: 6 } }}
disabled={isGeneratingQuery}
autoComplete="off"
placeholder="Ask a question in natural language and well generate the query for you."
aria-labelledby="copilot-textfield-label"
/>
{showPromptTeachingBubble && copilotTeachingBubbleVisible && (
<TeachingBubble
calloutProps={{ directionalHint: DirectionalHint.bottomCenter }}
target="#naturalLanguageInput"
hasCloseButton={true}
closeButtonAriaLabel="Close"
onDismiss={() => toggleCopilotTeachingBubbleVisible(false)}
hasSmallHeadline={true}
headline="Write a prompt"
>
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);
toggleCopilotTeachingBubbleVisible(false);
}}
style={{ color: "white", fontWeight: 600 }}
>
sample prompts
</Link>{" "}
or write your own query
</TeachingBubble>
)}
<IconButton
iconProps={{ iconName: "Send" }}
disabled={isGeneratingQuery || !userPrompt.trim()}
style={{ marginLeft: 8 }}
onClick={() => startGenerateQueryProcess()}
aria-label="Send"
/>
<div role="alert" aria-label={getAriaLabel()}>
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
</div>
{showSamplePrompts && (
<Callout
styles={{ root: { minWidth: 400, maxWidth: "70vw" } }}
target="#naturalLanguageInput"
isBeakVisible={false}
onDismiss={() => setShowSamplePrompts(false)}
directionalHintFixed={true}
directionalHint={DirectionalHint.bottomLeftEdge}
alignTargetEdge={true}
gapSpace={4}
>
<Stack>
{filteredHistories?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Recent
</Text>
{filteredHistories.map((history, i) => (
<DefaultButton
key={i}
onClick={() => {
setUserPrompt(history);
setShowSamplePrompts(false);
inputEdited.current = true;
}}
onRenderIcon={() => <Image src={RecentIcon} styles={{ root: { overflow: "unset" } }} />}
styles={promptStyles}
>
{history}
</DefaultButton>
))}
</Stack>
)}
{filteredSuggestedPrompts?.length > 0 && (
<Stack>
<Text
style={{
width: "100%",
fontSize: 14,
fontWeight: 600,
color: "#0078D4",
marginLeft: 16,
padding: "4px 0",
}}
>
Suggested Prompts
</Text>
{filteredSuggestedPrompts.map((prompt) => (
<DefaultButton
key={prompt.id}
onClick={() => {
setUserPrompt(prompt.text);
setShowSamplePrompts(false);
inputEdited.current = true;
}}
onRenderIcon={() => <Image src={HintIcon} />}
styles={promptStyles}
>
{prompt.text}
</DefaultButton>
))}
</Stack>
)}
{(filteredHistories?.length > 0 || filteredSuggestedPrompts?.length > 0) && (
<Stack>
<Separator
styles={{
root: {
selectors: { "::before": { background: "#E1DFDD" } },
padding: 0,
},
}}
/>
<Text
style={{
width: "100%",
fontSize: 14,
marginLeft: 16,
padding: "4px 0",
}}
>
Learn about{" "}
<Link target="_blank" href="https://aka.ms/cdb-copilot-writing">
writing effective prompts
</Link>
</Text>
</Stack>
)}
</Stack>
</Callout>
)}
</Stack>
<Stack style={{ margin: "8px 0" }}>
<Text style={{ fontSize: 12 }}>
AI-generated content can have mistakes. Make sure it&apos;s accurate and appropriate before using it.{" "}
<Link href="https://aka.ms/cdb-copilot-preview-terms" target="_blank" style={{ color: "#0072c9" }}>
Read preview terms
</Link>
{showErrorMessageBar && (
<MessageBar messageBarType={MessageBarType.error}>
{errorMessage ? errorMessage : "We ran into an error and were not able to execute query."}
</MessageBar>
)}
{showInvalidQueryMessageBar && (
<MessageBar
messageBarType={MessageBarType.info}
styles={{ root: { backgroundColor: "#F0F6FF" }, icon: { color: "#015CDA" } }}
>
We were unable to generate a query based upon the prompt provided. Please modify the prompt and try again.
For examples of how to write a good prompt, please read
<Link href="https://aka.ms/cdb-copilot-writing" target="_blank">
this article.
</Link>{" "}
Our content guidelines can be found
<Link href="https://aka.ms/cdb-query-copilot" target="_blank">
here.
</Link>
</MessageBar>
)}
</Text>
</Stack>
{showFeedbackBar && (
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
{showCallout && !hideFeedbackModalForLikedQueries && (
<Callout
role="status"
style={{ padding: 8 }}
target="#likeBtn"
onDismiss={() => {
setShowCallout(false);
SubmitFeedback({
params: {
generatedQuery: generatedQuery,
likeQuery: likeQuery,
description: "",
userPrompt: userPrompt,
},
explorer,
databaseId,
containerId,
mode: isSampleCopilotActive ? "Sample" : "User",
});
}}
directionalHint={DirectionalHint.topCenter}
>
<Text>
Thank you. Need to give{" "}
<Link
onClick={() => {
setShowCallout(false);
openFeedbackModal(generatedQuery, true, userPrompt);
}}
>
more feedback?
</Link>
</Text>
</Callout>
)}
<IconButton
id="likeBtn"
style={{ marginLeft: 20 }}
aria-label="Like"
role="toggle"
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
onClick={() => {
setShowCallout(!likeQuery);
setLikeQuery(!likeQuery);
if (likeQuery === true) {
document.getElementById("likeStatus").innerHTML = "Unpressed";
}
if (likeQuery === false) {
document.getElementById("likeStatus").innerHTML = "Liked";
}
if (dislikeQuery) {
setDislikeQuery(!dislikeQuery);
}
}}
/>
<IconButton
style={{ margin: "0 10px" }}
role="toggle"
aria-label="Dislike"
iconProps={{ iconName: dislikeQuery === true ? "DislikeSolid" : "Dislike" }}
onClick={() => {
let toggleStatusValue = "Unpressed";
if (!dislikeQuery) {
openFeedbackModal(generatedQuery, false, userPrompt);
setLikeQuery(false);
toggleStatusValue = "Disliked";
}
setDislikeQuery(!dislikeQuery);
setShowCallout(false);
document.getElementById("likeStatus").innerHTML = toggleStatusValue;
}}
/>
<span role="status" style={{ position: "absolute", left: "-9999px" }} id="likeStatus"></span>
<Separator vertical style={{ color: "#EDEBE9" }} />
<CommandBarButton
onClick={copyGeneratedCode}
iconProps={{ iconName: "Copy" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
>
Copy query
</CommandBarButton>
<CommandBarButton
onClick={() => {
setShowDeletePopup(true);
}}
iconProps={{ iconName: "Delete" }}
style={{ margin: "0 10px", backgroundColor: "#FFF8F0", transition: "background-color 0.3s ease" }}
>
Delete query
</CommandBarButton>
</Stack>
)}
<WelcomeModal visible={isWelcomModalVisible()} />
{isSamplePromptsOpen && <SamplePrompts sampleProps={sampleProps} />}
{query !== "" && query.trim().length !== 0 && (
<DeletePopup

View File

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

View File

@@ -42,11 +42,6 @@ function bindDataTable(element: any, valueAccessor: any, allBindings: any, viewM
createDataTable(0, tableEntityListViewModel, queryTablesTab); // Fake a DataTable to start.
$(window).resize(updateTableScrollableRegionMetrics);
operationManager.focusTable(); // Also selects the first row if needed.
// Attach the arrow key event handler to the table element
$dataTable.on("keydown", (event: JQueryEventObject) => {
handleArrowKey(element, valueAccessor, allBindings, viewModel, bindingContext, event);
});
}
function onTableColumnChange(enablePrompt: boolean = true, queryTablesTab: QueryTablesTab) {
@@ -215,39 +210,6 @@ function selectionChanged(element: any, valueAccessor: any, allBindings: any, vi
});
//selected = bindingContext.$data.selected();
}
function handleArrowKey(
element: any,
valueAccessor: any,
allBindings: any,
viewModel: any,
bindingContext: any,
event: JQueryEventObject,
) {
const isUpArrowKey: boolean = event.keyCode === Constants.keyCodes.UpArrow;
const isDownArrowKey: boolean = event.keyCode === Constants.keyCodes.DownArrow;
if (isUpArrowKey || isDownArrowKey) {
const $dataTable = $(element);
let $selectedRow = $dataTable.find("tr.selected");
if ($selectedRow.length === 0) {
// No row is currently selected, select the first row
$selectedRow = $dataTable.find("tr:first");
$selectedRow.addClass("selected");
} else {
const $targetRow = isUpArrowKey ? $selectedRow.prev("tr") : $selectedRow.next("tr");
if ($targetRow.length > 0) {
// Remove the selected class from the current row and add it to the target row
$selectedRow.removeClass("selected").attr("tabindex", "-1");
$targetRow.addClass("selected").attr("tabindex", "0");
$targetRow.focus();
}
}
event.preventDefault();
}
}
function dataChanged(element: any, valueAccessor: any, allBindings: any, viewModel: any, bindingContext: any) {
// do nothing for now

View File

@@ -19,7 +19,6 @@ import Explorer from "../Explorer";
import * as TableConstants from "./Constants";
import * as Entities from "./Entities";
import * as TableEntityProcessor from "./TableEntityProcessor";
import { CassandraProxyAPIs } from "../../Common/Constants";
export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[];
@@ -262,57 +261,6 @@ export class CassandraAPIDataClient extends TableDataClient {
query: string,
shouldNotify?: boolean,
paginationToken?: string,
): Promise<Entities.IListTableEntitiesResult> {
if (!this.useCassandraProxyEndpoint("postQuery")) {
return this.queryDocuments_ToBeDeprecated(collection, query, shouldNotify, paginationToken);
}
const clearMessage =
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
try {
const { authType, databaseAccount } = userContext;
const apiEndpoint: string =
authType === AuthType.EncryptedToken
? CassandraProxyAPIs.connectionStringQueryApi
: CassandraProxyAPIs.queryApi;
const data: any = await $.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, {
type: "POST",
contentType: Constants.ContentType.applicationJson,
data: JSON.stringify({
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, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
throw error;
} finally {
clearMessage?.();
}
}
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()}`);
@@ -346,11 +294,7 @@ export class CassandraAPIDataClient extends TableDataClient {
};
} catch (error) {
shouldNotify &&
handleError(
error,
"QueryDocuments_ToBeDeprecated_Cassandra",
`Failed to query rows for table ${collection.id()}`,
);
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
throw error;
} finally {
clearMessage?.();
@@ -458,50 +402,6 @@ export class CassandraAPIDataClient extends TableDataClient {
}
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
if (!this.useCassandraProxyEndpoint("getTableKeys")) {
return this.getTableKeys_ToBeDeprecated(collection);
}
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 ? CassandraProxyAPIs.connectionStringKeysApi : CassandraProxyAPIs.keysApi;
let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKeys>();
$.ajax(endpoint, {
type: "POST",
contentType: Constants.ContentType.applicationJson,
data: JSON.stringify({
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) => {
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(error);
},
)
.done(clearInProgressMessage);
return deferred.promise;
}
public getTableKeys_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
if (!!collection.cassandraKeys) {
return Q.resolve(collection.cassandraKeys);
}
@@ -542,51 +442,6 @@ export class CassandraAPIDataClient extends TableDataClient {
}
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
if (!this.useCassandraProxyEndpoint("getSchema")) {
return this.getTableSchema_ToBeDeprecated(collection);
}
if (!!collection.cassandraSchema) {
return Q.resolve(collection.cassandraSchema);
}
const clearInProgressMessage = logConsoleProgress(`Fetching schema for table ${collection.id()}`);
const { databaseAccount, authType } = userContext;
const apiEndpoint: string =
authType === AuthType.EncryptedToken
? CassandraProxyAPIs.connectionStringSchemaApi
: CassandraProxyAPIs.schemaApi;
let endpoint = `${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`;
const deferred = Q.defer<CassandraTableKey[]>();
$.ajax(endpoint, {
type: "POST",
contentType: Constants.ContentType.applicationJson,
data: JSON.stringify({
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) => {
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(error);
},
)
.done(clearInProgressMessage);
return deferred.promise;
}
public getTableSchema_ToBeDeprecated(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
if (!!collection.cassandraSchema) {
return Q.resolve(collection.cassandraSchema);
}
@@ -627,44 +482,6 @@ export class CassandraAPIDataClient extends TableDataClient {
}
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
if (!this.useCassandraProxyEndpoint("createOrDelete")) {
return this.createOrDeleteQuery_ToBeDeprecated(cassandraEndpoint, resourceId, query);
}
const deferred = Q.defer();
const { authType, databaseAccount } = userContext;
const apiEndpoint: string =
authType === AuthType.EncryptedToken
? CassandraProxyAPIs.connectionStringCreateOrDeleteApi
: CassandraProxyAPIs.createOrDeleteApi;
$.ajax(`${configContext.CASSANDRA_PROXY_ENDPOINT}/${apiEndpoint}`, {
type: "POST",
contentType: Constants.ContentType.applicationJson,
data: JSON.stringify({
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 createOrDeleteQuery_ToBeDeprecated(
cassandraEndpoint: string,
resourceId: string,
query: string,
): Q.Promise<any> {
const deferred = Q.defer();
const { authType, databaseAccount } = userContext;
const apiEndpoint: string =
@@ -730,19 +547,4 @@ export class CassandraAPIDataClient extends TableDataClient {
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
return collection.cassandraKeys.partitionKeys[0].property;
}
private useCassandraProxyEndpoint(api: string): boolean {
let canAccessCassandraProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (userContext.databaseAccount.properties.ipRules?.length > 0) {
canAccessCassandraProxy = canAccessCassandraProxy && configContext.CASSANDRA_PROXY_OUTBOUND_IPS_ALLOWLISTED;
}
return (
canAccessCassandraProxy &&
configContext.NEW_CASSANDRA_APIS?.includes(api) &&
[Constants.CassandraProxyEndpoints.Development, Constants.CassandraProxyEndpoints.Mpac].includes(
configContext.CASSANDRA_PROXY_ENDPOINT,
)
);
}
}

View File

@@ -1,5 +1,4 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
@@ -882,7 +881,7 @@ export default class DocumentsTab extends TabsBase {
}
protected getTabsButtons(): CommandButtonComponentProps[] {
if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) {
if (!userContext.hasWriteAccess) {
// All the following buttons require write access
return [];
}

View File

@@ -1,8 +1,8 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { Pivot, PivotItem } from "@fluentui/react";
import React from "react";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import DiscardIcon from "../../../../images/discard.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { NormalizedEventKey } from "../../../Common/Constants";
import { createStoredProcedure } from "../../../Common/dataAccess/createStoredProcedure";
@@ -512,12 +512,7 @@ export default class StoredProcedureTabComponent extends React.Component<
return (
<div className="tab-pane flexContainer stored-procedure-tab" role="tabpanel">
<div className="storedTabForm flexContainer">
<div className="formTitleFirst">
Stored Procedure Id
<span className="mandatoryStar" style={{ color: "#ff0707", fontSize: "14px", fontWeight: "bold" }}>
*&nbsp;
</span>
</div>
<div className="formTitleFirst">Stored Procedure Id</div>
<span className="formTitleTextbox">
<input
className="formTree"

View File

@@ -1,8 +1,5 @@
import { Link, MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { CassandraProxyEndpoints, MongoProxyEndpoints } from "Common/Constants";
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext, updateConfigContext } from "ConfigContext";
import { IpRule } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { CollectionTabKind } from "Contracts/ViewModels";
import Explorer from "Explorer/Explorer";
@@ -15,7 +12,6 @@ import { VcoreMongoConnectTab } from "Explorer/Tabs/VCoreMongoConnectTab";
import { VcoreMongoQuickstartTab } from "Explorer/Tabs/VCoreMongoQuickstartTab";
import { hasRUThresholdBeenConfigured } from "Shared/StorageUtility";
import { userContext } from "UserContext";
import { CassandraProxyOutboundIPs, MongoProxyOutboundIPs, PortalBackendIPs } from "Utils/EndpointUtils";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import ko from "knockout";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
@@ -35,12 +31,8 @@ interface TabsProps {
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
const [showRUThresholdMessageBar, setShowRUThresholdMessageBar] = useState<boolean>(
userContext.apiType === "SQL" && configContext.platform !== Platform.Fabric && !hasRUThresholdBeenConfigured(),
userContext.apiType === "SQL" && !hasRUThresholdBeenConfigured(),
);
const [
showMongoAndCassandraProxiesNetworkSettingsWarningState,
setShowMongoAndCassandraProxiesNetworkSettingsWarningState,
] = useState<boolean>(showMongoAndCassandraProxiesNetworkSettingsWarning());
return (
<div className="tabsManagerContainer">
{networkSettingsWarning && (
@@ -77,25 +69,9 @@ export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
},
}}
>
{`To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove
the limit, go to the Settings cog on the right and find "RU Threshold".`}
<Link
href="https://review.learn.microsoft.com/en-us/azure/cosmos-db/data-explorer?branch=main#configure-request-unit-threshold"
target="_blank"
>
Learn More
</Link>
</MessageBar>
)}
{showMongoAndCassandraProxiesNetworkSettingsWarningState && (
<MessageBar
messageBarType={MessageBarType.warning}
onDismiss={() => {
setShowMongoAndCassandraProxiesNetworkSettingsWarningState(false);
}}
>
{`We are moving our middleware to new infrastructure. To avoid future issues with Data Explorer access, please
re-enable "Allow access from Azure Portal" on the Networking blade for your account.`}
{
"To prevent queries from using excessive RUs, Data Explorer has a 5,000 RU default limit. To modify or remove the limit, go to the Settings cog on the right and find 'RU Threshold'."
}
</MessageBar>
)}
<div id="content" className="flexContainer hideOverflows">
@@ -182,9 +158,11 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
)}
</span>
<span className="tabNavText">{useObservable(tab?.tabTitle || getReactTabTitle())}</span>
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
</span>
{tabKind !== ReactTabKind.Home && (
<span className="tabIconSection">
<CloseButton tab={tab} active={active} hovering={hovering} tabKind={tabKind} />
</span>
)}
</div>
</span>
</li>
@@ -321,59 +299,3 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
throw Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
}
};
const showMongoAndCassandraProxiesNetworkSettingsWarning = (): boolean => {
const ipRules: IpRule[] = userContext.databaseAccount?.properties?.ipRules;
if ((userContext.apiType === "Mongo" || userContext.apiType === "Cassandra") && ipRules?.length) {
const legacyPortalBackendIPs: string[] = PortalBackendIPs[configContext.BACKEND_ENDPOINT];
const ipAddressesFromIPRules: string[] = ipRules.map((ipRule) => ipRule.ipAddressOrRange);
const ipRulesIncludeLegacyPortalBackend: boolean =
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => legacyPortalBackendIPs.includes(ipAddressFromIPRule))
?.length === legacyPortalBackendIPs.length;
if (!ipRulesIncludeLegacyPortalBackend) {
return false;
}
if (userContext.apiType === "Mongo") {
const isProdOrMpacMongoProxyEndpoint: boolean = [MongoProxyEndpoints.Mpac, MongoProxyEndpoints.Prod].includes(
configContext.MONGO_PROXY_ENDPOINT,
);
const mongoProxyOutboundIPs: string[] = isProdOrMpacMongoProxyEndpoint
? [...MongoProxyOutboundIPs[MongoProxyEndpoints.Mpac], ...MongoProxyOutboundIPs[MongoProxyEndpoints.Prod]]
: MongoProxyOutboundIPs[configContext.MONGO_PROXY_ENDPOINT];
const ipRulesIncludeMongoProxy: boolean =
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => mongoProxyOutboundIPs.includes(ipAddressFromIPRule))
?.length === mongoProxyOutboundIPs.length;
if (ipRulesIncludeMongoProxy) {
updateConfigContext({
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: true,
});
}
return !ipRulesIncludeMongoProxy;
} else if (userContext.apiType === "Cassandra") {
const isProdOrMpacCassandraProxyEndpoint: boolean = [
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
].includes(configContext.CASSANDRA_PROXY_ENDPOINT);
const cassandraProxyOutboundIPs: string[] = isProdOrMpacCassandraProxyEndpoint
? [
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Mpac],
...CassandraProxyOutboundIPs[CassandraProxyEndpoints.Prod],
]
: CassandraProxyOutboundIPs[configContext.CASSANDRA_PROXY_ENDPOINT];
const ipRulesIncludeCassandraProxy: boolean =
ipAddressesFromIPRules.filter((ipAddressFromIPRule) => cassandraProxyOutboundIPs.includes(ipAddressFromIPRule))
?.length === cassandraProxyOutboundIPs.length;
return !ipRulesIncludeCassandraProxy;
}
}
return false;
};

View File

@@ -40,10 +40,11 @@ export default class TabsBase extends WaitsForTemplateViewModel {
this.database = options.database;
this.rid = options.rid || (this.collection && this.collection.rid) || "";
this.tabKind = options.tabKind;
this.tabTitle = ko.observable<string>(this.getTitle(options));
this.tabTitle = ko.observable<string>(options.title);
this.tabPath =
this.collection &&
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${options.title}`);
ko.observable(options.tabPath ?? "") ||
(this.collection &&
ko.observable<string>(`${this.collection.databaseId}>${this.collection.id()}>${this.tabTitle()}`));
this.pendingNotification = ko.observable<DataModels.Notification>(undefined);
this.onLoadStartKey = options.onLoadStartKey;
this.closeTabButton = {
@@ -142,26 +143,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return (this.collection && this.collection.container) || (this.database && this.database.container);
}
public getTitle(options: ViewModels.TabOptions): string {
const coll = this.collection?.id();
const db = this.database?.id();
if (coll) {
if (coll.length > 8) {
return coll.slice(0, 5) + "…" + options.title;
} else {
return coll + "." + options.title;
}
} else if (db) {
if (db.length > 8) {
return db.slice(0, 5) + "…" + options.title;
} else {
return db + "." + options.title;
}
} else {
return options.title;
}
}
/** Renders a Javascript object to be displayed inside Monaco Editor */
public renderObjectForEditor(value: any, replacer: any, space: string | number): string {
return JSON.stringify(value, replacer, space);

View File

@@ -34,7 +34,6 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
private getTabId: () => string,
private getUsername: () => string,
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
private kind: ViewModels.TerminalKind,
) {}
public renderComponent(): JSX.Element {
@@ -43,7 +42,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
<QuickstartFirewallNotification
messageType={MessageTypes.OpenPostgresNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName={this.getShellNameForDisplay(this.kind)}
shellName="PostgreSQL"
/>
);
}
@@ -59,18 +58,6 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
);
}
private getShellNameForDisplay(terminalKind: ViewModels.TerminalKind): string {
switch (terminalKind) {
case ViewModels.TerminalKind.Postgres:
return "PostgreSQL";
case ViewModels.TerminalKind.Mongo:
case ViewModels.TerminalKind.VCoreMongo:
return "MongoDB";
default:
return "";
}
}
}
export default class TerminalTab extends TabsBase {
@@ -89,7 +76,6 @@ export default class TerminalTab extends TabsBase {
() => this.tabId,
() => this.getUsername(),
this.isAllPublicIPAddressesEnabled,
options.kind,
);
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if (

View File

@@ -58,7 +58,6 @@ export default class Collection implements ViewModels.Collection {
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public usageSizeInKB: ko.Observable<number>;
public computedProperties: ko.Observable<DataModels.ComputedProperties>;
public offer: ko.Observable<DataModels.Offer>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
@@ -122,7 +121,6 @@ export default class Collection implements ViewModels.Collection {
this.schema = data.schema;
this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig);
this.computedProperties = ko.observable(data.computedProperties);
this.partitionKeyPropertyHeaders = this.partitionKey?.paths;
this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => {
@@ -308,7 +306,7 @@ export default class Collection implements ViewModels.Collection {
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Items",
tabTitle: this.rawDataModel.id + " - Items",
});
this.documentIds([]);
@@ -316,7 +314,7 @@ export default class Collection implements ViewModels.Collection {
partitionKey: this.partitionKey,
documentIds: ko.observableArray<DocumentId>([]),
tabKind: ViewModels.CollectionTabKind.Documents,
title: "Items",
title: this.rawDataModel.id + " - Items",
collection: this,
node: this,
tabPath: `${this.databaseId}>${this.id()}>Documents`,

View File

@@ -1,6 +1,5 @@
import { initializeIcons } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
import * as React from "react";
import { render } from "react-dom";
import ChevronRight from "../images/chevron-right.svg";
@@ -33,8 +32,7 @@ const App: React.FunctionComponent = () => {
// For showing/hiding panel
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const config = useConfig();
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
useAADAuth();
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth();
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
const [connectionString, setConnectionString] = React.useState<string>();
@@ -138,10 +136,7 @@ const App: React.FunctionComponent = () => {
{!isLoggedIn && !encryptedTokenMetadata && (
<ConnectExplorer {...{ login, setEncryptedToken, setAuthType, connectionString, setConnectionString }} />
)}
{isLoggedIn && authFailure && <AadAuthorizationFailure {...{ authFailure }} />}
{isLoggedIn && !authFailure && (
<DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />
)}
{isLoggedIn && <DirectoryPickerPanel {...{ isOpen, dismissPanel, armToken, tenantId, switchTenant }} />}
</>
);
};

View File

@@ -29,12 +29,9 @@ const requestDatabaseResourceTokens = async (): Promise<void> => {
}
updateUserContext({
fabricContext: {
...userContext.fabricContext,
databaseConnectionInfo: fabricDatabaseConnectionInfo,
isReadOnly: true,
},
fabricContext: { ...userContext.fabricContext, databaseConnectionInfo: fabricDatabaseConnectionInfo },
databaseAccount: { ...userContext.databaseAccount },
hasWriteAccess: false, // TODO: receive from fabricDatabaseConnectionInfo
});
scheduleRefreshDatabaseResourceToken();
} catch (error) {

View File

@@ -1,52 +0,0 @@
.aadAuthFailureContainer {
height: 100%;
width: 100%;
}
.aadAuthFailureContainer .aadAuthFailureFormContainer {
display: -webkit-flex;
display: -ms-flexbox;
display: -ms-flex;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
height: 100%;
width: 100%;
}
.aadAuthFailureContainer .aadAuthFailure {
text-align: center;
display: -webkit-flex;
display: -ms-flexbox;
display: -ms-flex;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
justify-content: center;
height: 100%;
margin-bottom: 60px;
}
.aadAuthFailureContainer .aadAuthFailure .authFailureTitle {
font-size: 16px;
font-weight: 500;
color: #d12d2d;
margin: 16px 8px 8px 8px;
}
.aadAuthFailureContainer .aadAuthFailure .authFailureMessage {
font-size: 14px;
color: #393939;
margin: 16px 16px 16px 16px;
word-wrap: break-word;
white-space: pre-wrap;
}
.aadAuthFailureContainer .aadAuthFailure .authFailureLink {
margin: 8px;
font-size: 14px;
color: #0058ad;
cursor: pointer;
}
.aadAuthFailureContainer .aadAuthFailure .aadAuthFailureContent {
margin: 8px;
color: #393939;
}

View File

@@ -1,29 +0,0 @@
import { AadAuthFailure } from "hooks/useAADAuth";
import * as React from "react";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
import "../AadAuthorizationFailure.less";
interface Props {
authFailure: AadAuthFailure;
}
export const AadAuthorizationFailure: React.FunctionComponent<Props> = ({ authFailure }: Props) => {
return (
<div id="aadAuthFailure" className="aadAuthFailureContainer" style={{ display: "flex" }}>
<div className="aadAuthFailureFormContainer">
<div className="aadAuthFailure">
<p className="aadAuthFailureContent">
<img src={ConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="authFailureTitle">Authorization Failure</p>
<p className="authFailureMessage">{authFailure.failureMessage}</p>
{authFailure.failureLinkTitle && (
<p className="authFailureLink" onClick={authFailure.failureLinkAction}>
{authFailure.failureLinkTitle}
</p>
)}
</div>
</div>
</div>
);
};

View File

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

View File

@@ -50,25 +50,8 @@ export interface VCoreMongoConnectionParams {
interface FabricContext {
connectionId: string;
databaseConnectionInfo: FabricDatabaseConnectionInfo | undefined;
isReadOnly: boolean;
isVisible: boolean;
}
export type AdminFeedbackControlPolicy =
| "connectedExperiences"
| "policyAllowFeedback"
| "policyAllowSurvey"
| "policyAllowScreenshot"
| "policyAllowContact"
| "policyAllowContent"
| "policyEmailCollectionDefault"
| "policyScreenshotDefault"
| "policyContentSamplesDefault";
export type AdminFeedbackPolicySettings = {
[key in AdminFeedbackControlPolicy]: boolean;
};
interface UserContext {
readonly fabricContext?: FabricContext;
readonly authType?: AuthType;
@@ -100,11 +83,10 @@ interface UserContext {
collectionCreationDefaults: CollectionCreationDefaults;
sampleDataConnectionInfo?: ParsedResourceTokenConnectionString;
readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams;
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
}
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod1" | "rx" | "ex" | "prod" | "dev";
export type PortalEnv = "localhost" | "blackforest" | "fairfax" | "mooncake" | "prod" | "dev";
const ONE_WEEK_IN_MS = 604800000;

View File

@@ -1,11 +1,9 @@
import * as msal from "@azure/msal-browser";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext";
import * as ViewModels from "../Contracts/ViewModels";
import { traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext";
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
@@ -45,8 +43,8 @@ export function decryptJWTToken(token: string) {
return JSON.parse(tokenPayload);
}
export async function getMsalInstance() {
const msalConfig: msal.Configuration = {
export function getMsalInstance() {
const config: msal.Configuration = {
cache: {
cacheLocation: "localStorage",
},
@@ -57,46 +55,8 @@ export async function getMsalInstance() {
};
if (process.env.NODE_ENV === "development") {
msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
config.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
}
const msalInstance = new msal.PublicClientApplication(msalConfig);
const msalInstance = new msal.PublicClientApplication(config);
return msalInstance;
}
export async function acquireTokenWithMsal(msalInstance: msal.IPublicClientApplication, request: msal.SilentRequest) {
const tokenRequest = {
account: msalInstance.getActiveAccount() || null,
...request,
};
try {
// attempt silent acquisition first
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
} catch (silentError) {
if (silentError instanceof msal.InteractionRequiredAuthError) {
try {
// The error indicates that we need to acquire the token interactively.
// This will display a pop-up to re-establish authorization. If user does not
// have pop-ups enabled in their browser, this will fail.
return (await msalInstance.acquireTokenPopup(tokenRequest)).accessToken;
} catch (interactiveError) {
traceFailure(Action.SignInAad, {
request: JSON.stringify(tokenRequest),
acquireTokenType: "interactive",
errorMessage: JSON.stringify(interactiveError),
});
throw interactiveError;
}
} else {
traceFailure(Action.SignInAad, {
request: JSON.stringify(tokenRequest),
acquireTokenType: "silent",
errorMessage: JSON.stringify(silentError),
});
throw silentError;
}
}
}

View File

@@ -1,9 +1,9 @@
import { userContext } from "../UserContext";
export function isRunningOnNationalCloud(): boolean {
return !isRunningOnPublicCloud();
}
export function isRunningOnPublicCloud(): boolean {
return userContext?.portalEnv === "prod1" || userContext?.portalEnv === "prod";
return (
userContext.portalEnv === "blackforest" ||
userContext.portalEnv === "fairfax" ||
userContext.portalEnv === "mooncake"
);
}

View File

@@ -1,11 +1,4 @@
import {
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import { configContext } from "ConfigContext";
import { JunoEndpoints } from "Common/Constants";
import * as Logger from "../Common/Logger";
export function validateEndpoint(
@@ -74,16 +67,17 @@ export const PortalBackendIPs: { [key: string]: string[] } = {
//usnat: ["7.28.202.68"],
};
export const MongoProxyOutboundIPs: { [key: string]: string[] } = {
[MongoProxyEndpoints.Mpac]: ["20.245.81.54", "40.118.23.126"],
[MongoProxyEndpoints.Prod]: ["40.80.152.199", "13.95.130.121"],
[MongoProxyEndpoints.Fairfax]: ["52.244.176.112", "52.247.148.42"],
[MongoProxyEndpoints.Mooncake]: ["52.131.240.99", "143.64.61.130"],
};
export class MongoProxyEndpoints {
public static readonly Development: string = "https://localhost:7238";
public static readonly MPAC: string = "https://cdb-ms-mpac-mp.cosmos.azure.com";
public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com";
public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us";
public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn";
}
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
MongoProxyEndpoints.Development,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.MPAC,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
MongoProxyEndpoints.Mooncake,
@@ -97,29 +91,6 @@ export const allowedMongoProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> =
"https://localhost:12901",
];
export const allowedCassandraProxyEndpoints: ReadonlyArray<string> = [
CassandraProxyEndpoints.Development,
CassandraProxyEndpoints.Mpac,
CassandraProxyEndpoints.Prod,
CassandraProxyEndpoints.Fairfax,
CassandraProxyEndpoints.Mooncake,
];
export const allowedCassandraProxyEndpoints_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 CassandraProxyOutboundIPs: { [key: string]: string[] } = {
[CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"],
[CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"],
[CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"],
[CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"],
};
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
@@ -144,9 +115,3 @@ export const allowedJunoOrigins: ReadonlyArray<string> = [
];
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
export function usePortalBackendEndpoint(backendApi: BackendApi): boolean {
const activePortalBackendEndpoints: string[] = [PortalBackendEndpoints.Development];
const activeBackendApi: boolean = configContext.NEW_BACKEND_APIS?.includes(backendApi) || false;
return activeBackendApi && activePortalBackendEndpoints.includes(configContext.PORTAL_BACKEND_ENDPOINT as string);
}

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
*/
import { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request";
import * as Types from "./types";
const apiVersion = "2024-02-15-preview";
import { configContext } from "../../../../ConfigContext";
const apiVersion = "2023-09-15-preview";
/* Lists the Cassandra keyspaces under an existing Azure Cosmos DB database account. */
export async function listCassandraKeyspaces(

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
*/
import { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request";
import * as Types from "./types";
const apiVersion = "2024-02-15-preview";
import { configContext } from "../../../../ConfigContext";
const apiVersion = "2023-09-15-preview";
/* Retrieves the metrics determined by the given filter for the given database account and collection. */
export async function listMetrics(

View File

@@ -3,13 +3,13 @@
Run "npm run generateARMClients" to regenerate
Edting this file directly should be done with extreme caution as not to diverge from ARM REST specs
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2024-02-15-preview/cosmos-db.json
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2023-09-15-preview/cosmos-db.json
*/
import { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request";
import * as Types from "./types";
const apiVersion = "2024-02-15-preview";
import { configContext } from "../../../../ConfigContext";
const apiVersion = "2023-09-15-preview";
/* Retrieves the metrics determined by the given filter for the given collection, split by partition. */
export async function listMetrics(

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