Compare commits

..

25 Commits

Author SHA1 Message Date
Laurent Nguyen
77f895d343 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-28 11:19:33 +02:00
Laurent Nguyen
92073a5646 Start kernel when opening notebook tab (#168)
* Add Auto-start kernel epic

* Replace deprecated rxjs empty() with EMPTY constant

* Fix format
2020-08-28 09:51:44 +02:00
Laurent Nguyen
3bc2701356 Format and fix type issues 2020-08-27 16:43:39 +02:00
Laurent Nguyen
35dbaeea96 Add code completion implementation 2020-08-27 15:52:38 +02:00
Laurent Nguyen
18745a9ae6 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-27 09:13:52 +02:00
Jordi Bunster
5c84b3a7d4 Allow 'platform' only to be overriden (#167)
ConfigContext defines all kinds of URLs and what not, I'm not
sure about the security implications of allowing all this stuff to
be modifiable by just anyone.
2020-08-25 22:48:58 -07:00
victor-meng
3223ff7685 Move createDatabase to RP (#166) 2020-08-25 15:45:37 -07:00
Steve Faulkner
38732af907 Triple equal lint rule (#162) 2020-08-21 19:51:36 -05:00
Steve Faulkner
e837f574a8 Fix Telemetry for String Case (#163) 2020-08-21 14:38:30 -05:00
victor-meng
47a5c315b5 Move updateCollection to ARM (#152) 2020-08-21 11:24:01 -07:00
victor-meng
1c80ced259 Fix settings tab issue for cassandra and add feature flag (#159)
* Fix settings tab issue for cassandra and add feature flag

* Adding the rest of the changes....

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2020-08-19 18:11:43 -05:00
victor-meng
5e6ac78b7d Tables - ReadCollection: Switch back to using SDK (#157) 2020-08-19 17:54:23 -05:00
Steve Faulkner
999196193f Update Config context ARM endpoint (#158) 2020-08-19 13:20:10 -05:00
victor-meng
951289e190 Switch back to SDK for deleteDatabase and use RP for readCollections for table API (#155) 2020-08-18 11:04:37 -07:00
Tanuj Mittal
3279460cfd Update @azure/cosmos to 3.9.0 (#154) 2020-08-17 14:26:19 -07:00
victor-meng
07b9c1d1b7 Switch back to using SDK to read databases and collections for Mongo API (#153) 2020-08-17 11:42:02 -07:00
Tanuj Mittal
dde2ca75c4 Show submit button by default in GenericRightPaneComponent & other fixes (#144)
* Bug fixes

* Fix build
2020-08-17 11:39:11 -07:00
Laurent Nguyen
f44a3da568 Allow boolean partition key in graph visualization (#150)
* Allow boolean pk values to be displayed

* Add unit tests
2020-08-17 09:50:57 +02:00
Tanuj Mittal
22b2e1df48 Support empty offers for serverless accounts (#132)
* Add support for no offers when serverless accounts

* Add workaround for ignoring failed /offers when using connection string

* Revert @azure/cosmos upgrade
2020-08-14 17:45:13 -07:00
Steve Faulkner
2752d6af00 Create Cassandra Keyspace via ARM (#142) 2020-08-13 13:26:52 -05:00
Laurent Nguyen
5be6f982f9 Resolve merge conflict 2020-08-13 16:35:26 +02:00
Laurent Nguyen
4fc9393b76 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-13 16:30:46 +02:00
Laurent Nguyen
cb5fe5316e Fix handling numeric partition keys (#113)
* Fix pk extraction from documentId in g.V() case

* Add unit test, cleanup pk related unit tests
2020-08-13 12:00:11 +02:00
Laurent Nguyen
ee51e873b8 Fix build issues 2020-07-13 13:52:23 +02:00
Laurent Nguyen
206a8ef93b New Monaco Editor 2020-07-13 13:52:05 +02:00
77 changed files with 2749 additions and 1169 deletions

View File

@@ -41,6 +41,7 @@ module.exports = {
"@typescript-eslint/no-extraneous-class": "error",
"no-null/no-null": "error",
"@typescript-eslint/no-explicit-any": "error",
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }]
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error"
}
};

24
package-lock.json generated
View File

@@ -5,13 +5,14 @@
"requires": true,
"dependencies": {
"@azure/cosmos": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.4.tgz",
"integrity": "sha512-IbSEadapQDajSCXj7gUc8OklkOd/oAY4w7XBLHouWc4iKQTtntb2DmGjhrbh2W5Ku+pmBSr1GTApCjQ55iIjlQ==",
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.9.0.tgz",
"integrity": "sha512-SA+QB54I8Dvg/ZolHpsEDLK/sbSB9sFmSU1ElnMTFw88TVik+LYHq4o/srU2TY6Gr1BketjPmgLVEqrmnRvjkw==",
"requires": {
"@types/debug": "^4.1.4",
"debug": "^4.1.1",
"fast-json-stable-stringify": "^2.0.0",
"jsbi": "^3.1.3",
"node-abort-controller": "^1.0.4",
"node-fetch": "^2.6.0",
"os-name": "^3.1.0",
@@ -22,14 +23,14 @@
},
"dependencies": {
"tslib": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
},
"uuid": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
}
}
},
@@ -20204,6 +20205,11 @@
"esprima": "^4.0.0"
}
},
"jsbi": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz",
"integrity": "sha512-nBJqA0C6Qns+ZxurbEoIR56wyjiUszpNy70FHvxO5ervMoCbZVE3z3kxr5nKGhlxr/9MhKTSUBs7cAwwuf3g9w=="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",

View File

@@ -4,7 +4,7 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/cosmos": "3.7.4",
"@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1",

View File

@@ -134,6 +134,7 @@ export class Features {
public static readonly enableAutoPilotV2 = "enableautopilotv2";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSDKoperations = "enablesdkoperations";
}
export class AfecFeatures {

View File

@@ -6,15 +6,13 @@ import * as ViewModels from "../Contracts/ViewModels";
import Q from "q";
import {
ConflictDefinition,
ContainerDefinition,
ContainerResponse,
DatabaseResponse,
FeedOptions,
ItemDefinition,
PartitionKeyDefinition,
QueryIterator,
Resource,
TriggerDefinition
TriggerDefinition,
OfferDefinition
} from "@azure/cosmos";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
import { client } from "./CosmosClient";
@@ -202,23 +200,6 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
return [partitionKeyValue];
}
export function updateCollection(
databaseId: string,
collectionId: string,
newCollection: DataModels.Collection,
options: any = {}
): Q.Promise<DataModels.Collection> {
return Q(
client()
.database(databaseId)
.container(collectionId)
.replace(newCollection as ContainerDefinition, options)
.then(async (response: ContainerResponse) => {
return refreshCachedResources().then(() => response.resource as DataModels.Collection);
})
);
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
@@ -244,7 +225,8 @@ export function updateOffer(
return Q(
client()
.offer(offer.id)
.replace(newOffer, options)
// 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)
.then(response => {
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
})
@@ -454,6 +436,10 @@ export function readCollectionQuotaInfo(
}
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
if (options.isServerless) {
return Q([]); // Reading offers is not supported for serverless accounts
}
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
@@ -469,6 +455,13 @@ export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
.offers.readAll()
.fetchAll()
.then(response => response.resources)
.catch(error => {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
throw error;
})
);
}
@@ -550,26 +543,6 @@ export function getOrCreateDatabaseAndCollection(
);
}
export function createDatabase(
request: DataModels.CreateDatabaseRequest,
options: any
): Q.Promise<DataModels.Database> {
var deferred = Q.defer<DataModels.Database>();
_createDatabase(request, options).then(
(createdDatabase: DataModels.Database) => {
refreshCachedOffers().then(() => {
deferred.resolve(createdDatabase);
});
},
_createDatabaseError => {
deferred.reject(_createDatabaseError);
}
);
return deferred.promise;
}
export function refreshCachedOffers(): Q.Promise<void> {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
@@ -598,33 +571,3 @@ export function queryConflicts(
.conflicts.query(query, options);
return Q(documentsIterator);
}
function _createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise<DataModels.Database> {
const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request;
const createBody: DatabaseRequest = { id: databaseId };
const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput");
// TODO: replace when SDK support autopilot
const initialHeaders = autoPilot
? !hasAutoPilotV2FeatureFlag
? {
[Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({ maxThroughput: autoPilot.maxThroughput })
}
: {
[Constants.HttpHeaders.autoPilotTier]: autoPilot.autopilotTier
}
: undefined;
if (!!databaseLevelThroughput) {
if (autoPilot) {
databaseOptions.initialHeaders = initialHeaders;
}
createBody.throughput = offerThroughput;
}
return Q(
client()
.databases.create(createBody, databaseOptions)
.then((response: DatabaseResponse) => {
return refreshCachedResources(databaseOptions).then(() => response.resource);
})
);
}

View File

@@ -266,42 +266,6 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
return deferred.promise;
}
export function updateCollection(
databaseId: string,
collection: ViewModels.Collection,
newCollection: DataModels.Collection
): Q.Promise<DataModels.Collection> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating container ${collection.id()}`
);
DataAccessUtilityBase.updateCollection(databaseId, collection.id(), newCollection)
.then(
(replacedCollection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated container ${collection.id()}`
);
deferred.resolve(replacedCollection);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to update container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
@@ -926,36 +890,3 @@ export function getOrCreateDatabaseAndCollection(
return deferred.promise;
}
export function createDatabase(
request: DataModels.CreateDatabaseRequest,
options: any = {}
): Q.Promise<DataModels.Database> {
const deferred: Q.Deferred<DataModels.Database> = Q.defer<DataModels.Database>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating a new database ${request.databaseId}`
);
DataAccessUtilityBase.createDatabase(request, options)
.then(
(database: DataModels.Database) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created database ${request.databaseId}`
);
deferred.resolve(database);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while creating database ${request.databaseId}:\n ${JSON.stringify(error)}`
);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
return deferred.promise;
}

View File

@@ -0,0 +1,46 @@
import "jquery";
import * as Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { userContext } from "../UserContext";
export class NotificationsClientBase {
private _extensionEndpoint: string;
private _notificationsApiSuffix: string;
protected constructor(notificationsApiSuffix: string) {
this._notificationsApiSuffix = notificationsApiSuffix;
}
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
const databaseAccount = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const url = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers: any = {};
headers[authorizationHeader.header] = authorizationHeader.token;
$.ajax({
url: url,
type: "GET",
headers: headers,
cache: false
}).then(
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
deferred.resolve(notifications);
},
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
deferred.reject(xhr.responseText);
}
);
return deferred.promise;
}
public setExtensionEndpoint(extensionEndpoint: string): void {
this._extensionEndpoint = extensionEndpoint;
}
}

View File

@@ -0,0 +1,250 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DatabaseResponse } from "@azure/cosmos";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import {
SqlDatabaseCreateUpdateParameters,
CreateUpdateOptions
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import {
createUpdateCassandraKeyspace,
getCassandraKeyspace
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateMongoDBDatabase,
getMongoDBDatabase
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import {
createUpdateGremlinDatabase,
getGremlinDatabase
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
let database: DataModels.Database;
const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`);
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
database = await createDatabaseWithARM(params);
} else {
database = await createDatabaseWithSDK(params);
}
} catch (error) {
logConsoleError(`Error while creating database ${params.databaseId}:\n ${error.message}`);
logError(JSON.stringify(error), "CreateDatabase", error.code);
sendNotificationForError(error);
clearMessage();
throw error;
}
logConsoleInfo(`Successfully created database ${params.databaseId}`);
await refreshCachedResources();
await refreshCachedOffers();
clearMessage();
return database;
}
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return createSqlDatabase(params);
case DefaultAccountExperienceType.MongoDB:
return createMongoDatabase(params);
case DefaultAccountExperienceType.Cassandra:
return createCassandraKeyspace(params);
case DefaultAccountExperienceType.Graph:
return createGremlineDatabase(params);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateSqlDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getMongoDBDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateMongoDBDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
try {
const getResponse = await getGremlinDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
},
options
}
};
const createResponse = await createUpdateGremlinDatabase(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Database);
}
async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
const createBody: DatabaseRequest = { id: params.databaseId };
const databaseOptions: RequestOptions = {};
// TODO: replace when SDK support autopilot
if (params.databaseLevelThroughput) {
if (params.autoPilotMaxThroughput) {
createBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createBody.throughput = params.offerThroughput;
}
}
const response: DatabaseResponse = await client().databases.create(createBody, databaseOptions);
return response.resource;
}
function constructRpOptions(params: DataModels.CreateDatabaseParams): CreateUpdateOptions {
if (!params.databaseLevelThroughput) {
return {};
}
if (params.autoPilotMaxThroughput) {
return {
autoscaleSettings: {
maxThroughput: params.autoPilotMaxThroughput
}
};
}
return {
throughput: params.offerThroughput
};
}

View File

@@ -15,7 +15,7 @@ import { refreshCachedResources } from "../DataAccessUtilityBase";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try {
if (window.authType === AuthType.AAD) {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteCollectionWithARM(databaseId, collectionId);
} else {
await client()

View File

@@ -15,7 +15,11 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
try {
if (window.authType === AuthType.AAD) {
if (
window.authType === AuthType.AAD &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
!userContext.useSDKOperations
) {
await deleteDatabaseWithARM(databaseId);
} else {
await client()

View File

@@ -16,7 +16,12 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
let collections: DataModels.Collection[];
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try {
if (window.authType === AuthType.AAD) {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
collections = await readCollectionsWithARM(databaseId);
} else {
const sdkResponse = await client()

View File

@@ -15,7 +15,13 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`);
try {
if (window.authType === AuthType.AAD) {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
) {
databases = await readDatabasesWithARM();
} else {
const sdkResponse = await client()

View File

@@ -0,0 +1,225 @@
import { AuthType } from "../../AuthType";
import { Collection } from "../../Contracts/DataModels";
import { ContainerDefinition } from "@azure/cosmos";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import {
ExtendedResourceProperties,
SqlContainerCreateUpdateParameters,
SqlContainerResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import {
createUpdateCassandraTable,
getCassandraTable
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import {
createUpdateGremlinGraph,
getGremlinGraph
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function updateCollection(
databaseId: string,
collectionId: string,
newCollection: Collection,
options: RequestOptions = {}
): Promise<Collection> {
let collection: Collection;
const clearMessage = logConsoleProgress(`Updating container ${collectionId}`);
try {
if (
window.authType === AuthType.AAD &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
} else {
const sdkResponse = await client()
.database(databaseId)
.container(collectionId)
.replace(newCollection as ContainerDefinition, options);
collection = sdkResponse.resource as Collection;
}
} catch (error) {
logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
throw error;
}
logConsoleInfo(`Successfully updated container ${collectionId}`);
clearMessage();
await refreshCachedResources();
return collection;
}
async function updateCollectionWithARM(
databaseId: string,
collectionId: string,
newCollection: Collection
): Promise<Collection> {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return updateSqlContainer(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.MongoDB:
return updateMongoDBCollection(
databaseId,
collectionId,
subscriptionId,
resourceGroup,
accountName,
newCollection
);
case DefaultAccountExperienceType.Cassandra:
return updateCassandraTable(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Graph:
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
case DefaultAccountExperienceType.Table:
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
}
async function updateSqlContainer(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateSqlContainer(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
}
async function updateMongoDBCollection(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateMongoDBCollection(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(
`MongoDB collection to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
);
}
async function updateCassandraTable(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateCassandraTable(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(
`Cassandra table to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
);
}
async function updateGremlinGraph(
databaseId: string,
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateGremlinGraph(
subscriptionId,
resourceGroup,
accountName,
databaseId,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(`Gremlin graph to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
}
async function updateTable(
collectionId: string,
subscriptionId: string,
resourceGroup: string,
accountName: string,
newCollection: Collection
): Promise<Collection> {
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
if (getResponse && getResponse.properties && getResponse.properties.resource) {
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
const updateResponse = await createUpdateTable(
subscriptionId,
resourceGroup,
accountName,
collectionId,
getResponse as SqlContainerCreateUpdateParameters
);
return updateResponse && (updateResponse.properties.resource as Collection);
}
throw new Error(`Table to update does not exist. Table id: ${collectionId}`);
}

View File

@@ -80,12 +80,20 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
console.error(error);
}
}
// Allow override of any config value with URL query parameters
// Allow override of platform value with URL query parameter
const params = new URLSearchParams(window.location.search);
params.forEach((value, key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(configContext as any)[key] = value;
});
if (params.has("platform")) {
const platform = params.get("platform");
switch (platform) {
default:
console.log("Invalid platform query parameter given, ignoring");
break;
case Platform.Portal:
case Platform.Hosted:
case Platform.Emulator:
updateConfigContext({ platform });
}
}
} catch (error) {
console.log("No configuration file found using defaults");
}

View File

@@ -153,7 +153,14 @@ export interface KeyResource {
Token: string;
}
export interface IndexingPolicy {}
export interface IndexingPolicy {
automatic: boolean;
indexingMode: string;
includedPaths: any;
excludedPaths: any;
compositeIndexes?: any;
spatialIndexes?: any;
}
export interface PartitionKey {
paths: string[];
@@ -320,12 +327,11 @@ export interface AutoPilotOfferSettings {
targetMaxThroughput?: number;
}
export interface CreateDatabaseRequest {
export interface CreateDatabaseParams {
autoPilotMaxThroughput?: number;
databaseId: string;
databaseLevelThroughput?: boolean;
offerThroughput?: number;
autoPilot?: AutoPilotCreationSettings;
hasAutoPilotV2FeatureFlag?: boolean;
}
export interface SharedThroughputRange {

View File

@@ -37,7 +37,7 @@ import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { configContext } from "../ConfigContext";
import { configContext, updateConfigContext } from "../ConfigContext";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
@@ -82,6 +82,7 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import { NotificationsClientBase } from "../Common/NotificationsClientBase";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
@@ -97,6 +98,7 @@ enum ShareAccessToggleState {
}
interface ExplorerOptions {
notificationsClient: NotificationsClientBase;
isEmulator: boolean;
}
interface AdHocAccessData {
@@ -131,6 +133,7 @@ export default class Explorer {
public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>;
public isEmulator: boolean;
public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>;
@@ -138,6 +141,7 @@ export default class Explorer {
public extensionEndpoint: ko.Observable<string>;
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public notificationsClient: NotificationsClientBase;
public queriesClient: QueriesClient;
public tableDataClient: TableDataClient;
public splitter: Splitter;
@@ -265,7 +269,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor() {
constructor(options: ExplorerOptions) {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree
});
@@ -371,6 +375,8 @@ export default class Explorer {
}
});
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.notificationsClient = options.notificationsClient;
this.isEmulator = options.isEmulator;
this.features = ko.observable();
this.serverId = ko.observable<string>();
@@ -969,6 +975,10 @@ export default class Explorer {
this.sparkClusterConnectionInfo.valueHasMutated();
}
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
updateUserContext({ useSDKOperations: true });
}
featureSubcription.dispose();
});
@@ -1469,38 +1479,33 @@ export default class Explorer {
);
};
if (this.isServerlessEnabled()) {
// Serverless accounts don't support offers call
refreshDatabases();
} else {
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers();
this._setLoadingStatusText("Fetching offers...");
offerPromise.then(
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
refreshDatabases(offers);
},
error => {
this._setLoadingStatusText("Failed to fetch offers.");
this.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: JSON.stringify(error)
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${JSON.stringify(error)}`
);
}
);
}
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
this._setLoadingStatusText("Fetching offers...");
offerPromise.then(
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
refreshDatabases(offers);
},
error => {
this._setLoadingStatusText("Failed to fetch offers.");
this.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: JSON.stringify(error)
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${JSON.stringify(error)}`
);
}
);
return deferred.promise.then(
() => {
@@ -1933,6 +1938,7 @@ export default class Explorer {
this.serverId(inputs.serverId);
this.extensionEndpoint(inputs.extensionEndpoint || "");
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.notificationsClient.setExtensionEndpoint(this.extensionEndpoint());
this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType);
this.quotaId(inputs.quotaId);
@@ -1947,12 +1953,17 @@ export default class Explorer {
this._importExplorerConfigComplete = true;
updateConfigContext({
ARM_ENDPOINT: this.armEndpoint()
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId
});
updateUserContext({ resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId });
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
{

View File

@@ -87,13 +87,31 @@ describe("getPkIdFromDocumentId", () => {
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
});
it("should create pkid pair from partitioned graph (pk as number)", () => {
const doc = createFakeDoc({ id: "id", mypk: 234 });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[234, 'id']");
});
it("should create pkid pair from partitioned graph (pk as boolean)", () => {
const doc = createFakeDoc({ id: "id", mypk: true });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[true, 'id']");
});
it("should create pkid pair from partitioned graph (pk as valid array value)", () => {
const doc = createFakeDoc({ id: "id", mypk: [{ id: "someid", _value: "pkvalue" }] });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
});
it("should error if id is not a string", () => {
const doc = createFakeDoc({ id: { foo: 1 } });
it("should error if id is not a string or number", () => {
let doc = createFakeDoc({ id: { foo: 1 } });
try {
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
expect(true).toBe(false);
} catch (e) {
expect(true).toBe(true);
}
doc = createFakeDoc({ id: true });
try {
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
expect(true).toBe(false);
@@ -102,16 +120,8 @@ describe("getPkIdFromDocumentId", () => {
}
});
it("should error if pk not string nor non-empty array", () => {
let doc = createFakeDoc({ mypk: { foo: 1 } });
try {
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
} catch (e) {
expect(true).toBe(true);
}
doc = createFakeDoc({ mypk: [] });
it("should error if pk is empty array", () => {
let doc = createFakeDoc({ mypk: [] });
try {
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
expect(true).toBe(false);

View File

@@ -1371,7 +1371,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
let pk = (d as any)[collectionPartitionKeyProperty];
if (typeof pk !== "string") {
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
if (Array.isArray(pk) && pk.length > 0) {
// pk is [{ id: 'id', _value: 'value' }]
pk = pk[0]["_value"];

View File

@@ -194,7 +194,7 @@ export class CommandBarComponentButtonFactory {
buttons.push(fullScreenButton);
}
if (configContext.platform !== Platform.Emulator) {
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,

View File

@@ -0,0 +1,464 @@
import { Channels } from "@nteract/messaging";
import * as monaco from "./monaco";
import * as React from "react";
import { completionProvider } from "./completions/completionItemProvider";
import { AppState, ContentRef } from "@nteract/core";
import { connect } from "react-redux";
import "./styles.css";
import { LightThemeName, HCLightThemeName, DarkThemeName } from "./theme";
// import { logger } from "src/common/localLogger";
import { getCellMonacoLanguage } from "./selectors";
// import { DocumentUri } from "./documentUri";
export type IModelContentChangedEvent = monaco.editor.IModelContentChangedEvent;
/**
* Initial props for Monaco received from agnostic component
*/
export interface IMonacoProps {
id: string;
contentRef: ContentRef;
modelUri?: monaco.Uri;
theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string;
cellLanguageOverride?: string;
notebookLanguageOverride?: string;
readOnly?: boolean;
channels: Channels | undefined;
enableCompletion: boolean;
shouldRegisterDefaultCompletion?: boolean;
onChange: (value: string, event?: unknown) => void;
onFocusChange: (focus: boolean) => void;
onCursorPositionChange?: (selection: monaco.ISelection | null) => void;
onRegisterCompletionProvider?: (languageId: string) => void;
value: string;
editorFocused: boolean;
lineNumbers: boolean;
/** set height of editor to fit the specified number of lines in display */
numberOfLines?: number;
options?: monaco.editor.IEditorOptions;
}
/**
* Monaco specific props derived from State
*/
interface IMonacoStateProps {
language: string;
}
// Cache the custom theme data to avoid repeatly defining the custom theme
let customThemeData: monaco.editor.IStandaloneThemeData;
function getMonacoTheme(theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string) {
if (typeof theme === "string") {
switch (theme) {
case "vs-dark":
return DarkThemeName;
case "hc-black":
return "hc-black";
case "vs":
return LightThemeName;
case "hc-light":
return HCLightThemeName;
default:
return LightThemeName;
}
} else if (theme === undefined || typeof theme === "undefined") {
return LightThemeName;
} else {
const themeName = "custom-vs";
// Skip redefining the same custom theme if it is the same theme data.
if (customThemeData !== theme) {
monaco.editor.defineTheme(themeName, theme);
customThemeData = theme;
}
return themeName;
}
}
const makeMapStateToProps = (initialState: AppState, initialProps: IMonacoProps) => {
const { id, contentRef } = initialProps;
const mapStateToProps = (state: AppState, ownProps: IMonacoProps & IMonacoStateProps) => {
return {
language: getCellMonacoLanguage(
state,
contentRef,
id,
ownProps.cellLanguageOverride,
ownProps.notebookLanguageOverride
)
};
};
return mapStateToProps;
};
/**
* Creates a MonacoEditor instance within the MonacoContainer div
*/
export class MonacoEditor extends React.Component<IMonacoProps & IMonacoStateProps> {
editor?: monaco.editor.IStandaloneCodeEditor;
editorContainerRef = React.createRef<HTMLDivElement>();
contentHeight?: number;
private cursorPositionListener?: monaco.IDisposable;
constructor(props: IMonacoProps & IMonacoStateProps) {
super(props);
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
this.calculateHeight = this.calculateHeight.bind(this);
}
onDidChangeModelContent(e: monaco.editor.IModelContentChangedEvent): void {
if (this.editor) {
if (this.props.onChange) {
this.props.onChange(this.editor.getValue(), e);
}
this.calculateHeight();
}
}
/**
* Adjust the height of editor
*
* @remarks
* The way to determine how many lines we should display in editor:
* If numberOfLines is not set or set to 0, we adjust the height to fit the content
* If numberOfLines is specified we respect that setting
*/
calculateHeight(): void {
// Make sure we have an editor
if (!this.editor) {
return;
}
// Make sure we have a model
const model = this.editor.getModel();
if (!model) {
return;
}
if (this.editorContainerRef && this.editorContainerRef.current) {
const expectedLines = this.props.numberOfLines || model.getLineCount();
// The find & replace menu takes up 2 lines, that is why 2 line is set as the minimum number of lines
// TODO: we should either disable the find/replace menu or auto expand the editor when find/replace is triggerred.
const finalizedLines = Math.max(expectedLines, 1) + 1;
const lineHeight = this.editor.getConfiguration().lineHeight;
const contentHeight = finalizedLines * lineHeight;
if (this.contentHeight !== contentHeight) {
this.editorContainerRef.current.style.height = contentHeight + "px";
this.editor.layout();
this.contentHeight = contentHeight;
}
}
}
componentDidMount(): void {
if (this.editorContainerRef && this.editorContainerRef.current) {
// Register Jupyter completion provider if needed
this.registerCompletionProvider();
// Use Monaco model uri if provided. Otherwise, create a new model uri using editor id.
const uri = this.props.modelUri ? this.props.modelUri : monaco.Uri.file(this.props.id);
// Only create a new model if it does not exist. For example, when we double click on a markdown cell,
// an editor model is created for it. Once we go back to markdown preview mode that doesn't use the editor,
// double clicking on the markdown cell will again instantiate a monaco editor. In that case, we should
// rebind the previously created editor model for the markdown instead of recreating one. Monaco does not
// allow models to be recreated with the same uri.
let model = monaco.editor.getModel(uri);
if (!model) {
model = monaco.editor.createModel(this.props.value, this.props.language, uri);
}
// Create Monaco editor backed by a Monaco model.
this.editor = monaco.editor.create(this.editorContainerRef.current, {
// Following are the default settings
minimap: {
enabled: false
},
autoIndent: true,
overviewRulerLanes: 1,
scrollbar: {
useShadows: false,
verticalHasArrows: false,
horizontalHasArrows: false,
vertical: "hidden",
horizontal: "hidden",
verticalScrollbarSize: 0,
horizontalScrollbarSize: 0,
arrowSize: 30
},
scrollBeyondLastLine: false,
find: {
// TODO Need this?
// addExtraSpaceOnTop: false, // pops the editor out of alignment if turned on
seedSearchStringFromSelection: true, // default is true
autoFindInSelection: false // default is false
},
// Disable highlight current line, too much visual noise with it on.
// VS Code also has it disabled for their notebook experience.
renderLineHighlight: "none",
// Allow editor pop up widgets such as context menus, signature help, hover tips to be able to be
// displayed outside of the editor. Without this, the pop up widgets can be clipped.
fixedOverflowWidgets: true,
// Apply custom settings from configuration
...this.props.options,
// Apply specific settings passed-in as direct props
model,
value: this.props.value,
language: this.props.language,
readOnly: this.props.readOnly,
lineNumbers: this.props.lineNumbers ? "on" : "off",
theme: getMonacoTheme(this.props.theme)
});
this.addEditorTopMargin();
// Ignore Ctrl + Enter
// tslint:disable-next-line no-bitwise
this.editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
() => {
// Do nothing. This is handled elsewhere, we just don't want the editor to put the newline.
},
undefined
);
// TODO Add right context
this.toggleEditorOptions(this.props.editorFocused);
if (this.props.editorFocused) {
if (!this.editor.hasTextFocus()) {
// Bring browser focus to the editor if text not already in focus
this.editor.focus();
}
this.registerCursorListener();
}
// TODO: Need to remove the event listener when the editor is disposed, or we have a memory leak here.
// The same applies to the other event listeners below
// Adds listener under the resize window event which calls the resize method
window.addEventListener("resize", this.resize.bind(this));
// Adds listeners for undo and redo actions emitted from the toolbar
this.editorContainerRef.current.addEventListener("undo", () => {
if (this.editor) {
this.editor.trigger("undo-event", "undo", {});
}
});
this.editorContainerRef.current.addEventListener("redo", () => {
if (this.editor) {
this.editor.trigger("redo-event", "redo", {});
}
});
this.editor.onDidChangeModelContent(this.onDidChangeModelContent.bind(this));
this.editor.onDidFocusEditorText(this.onFocus);
this.editor.onDidBlurEditorText(this.onBlur);
this.calculateHeight();
// FIXME: This might need further investigation as the props value should be respected in construction
// The following is a mitigation measure till that time
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
this.editor.setValue(this.props.value);
}
}
addEditorTopMargin(): void {
if (this.editor) {
// Monaco editor doesn't have margins
// https://github.com/notable/notable/issues/551
// This is a workaround to add an editor area 12px padding at the top
// so that cursors rendered by collab decorators could be visible without being cut.
this.editor.changeViewZones(changeAccessor => {
const domNode = document.createElement("div");
changeAccessor.addZone({
afterLineNumber: 0,
heightInPx: 12,
domNode
});
});
}
}
/**
* Tells editor to check the surrounding container size and resize itself appropriately
*/
resize(): void {
if (this.editor && this.props.editorFocused) {
this.editor.layout();
}
}
componentDidUpdate(): void {
if (!this.editor) {
return;
}
const { value, channels, /* language, contentRef, id,*/ editorFocused, theme } = this.props;
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
if (this.editor.getValue() !== value) {
this.editor.setValue(value);
}
completionProvider.setChannels(channels);
// Register Jupyter completion provider if needed
this.registerCompletionProvider();
/*
// Apply new model to the editor when the language is changed.
const model = this.editor.getModel();
if (model && language && model.getModeId() !== language) {
const newUri = DocumentUri.createCellUri(contentRef, id, language);
if (!monaco.editor.getModel(newUri)) {
// Save the cursor position before we set new model.
const position = this.editor.getPosition();
// Set new model targeting the changed language.
this.editor.setModel(monaco.editor.createModel(value, language, newUri));
this.addEditorTopMargin();
// Restore cursor position to new model.
if (position) {
this.editor.setPosition(position);
}
// Dispose of the old model in a seperate event. We cannot dispose of the model within the
// componentDidUpdate method or else the editor will throw an exception. Zero in the timeout field
// means execute immediately but in a seperate next event.
setTimeout(() => model.dispose(), 0);
}
}
*/
if (theme) {
monaco.editor.setTheme(getMonacoTheme(theme));
}
// In the multi-tabs scenario, when the notebook is hidden by setting "display:none",
// Any state update propagated here would cause a UI re-layout, monaco-editor will then recalculate
// and set its height to 5px.
// To work around that issue, we skip updating the UI when parent element's offsetParent is null (which
// indicate an ancient element is hidden by display set to none)
// We may revisit this when we get to refactor for multi-notebooks.
if (!this.editorContainerRef.current?.offsetParent) {
return;
}
// Set focus
if (editorFocused && !this.editor.hasTextFocus()) {
this.editor.focus();
}
// Tells the editor pane to check if its container has changed size and fill appropriately
this.editor.layout();
}
componentWillUnmount(): void {
if (this.editor) {
try {
const model = this.editor.getModel();
if (model) {
model.dispose();
}
this.editor.dispose();
} catch (err) {
console.error(`Error occurs in disposing editor: ${JSON.stringify(err)}`);
}
}
}
render(): JSX.Element {
return (
<div className="monaco-container">
<div ref={this.editorContainerRef} id={`editor-${this.props.id}`} />
</div>
);
}
/**
* Register default kernel-based completion provider.
* @param language Language
*/
registerDefaultCompletionProvider(language: string): void {
// onLanguage event is emitted only once per language when language is first time needed.
monaco.languages.onLanguage(language, () => {
monaco.languages.registerCompletionItemProvider(language, completionProvider);
});
}
private onFocus() {
this.props.onFocusChange(true);
this.toggleEditorOptions(true);
this.registerCursorListener();
}
private onBlur() {
this.props.onFocusChange(false);
this.toggleEditorOptions(false);
this.unregisterCursorListener();
}
private registerCursorListener() {
if (this.editor && this.props.onCursorPositionChange) {
const selection = this.editor.getSelection();
this.props.onCursorPositionChange(selection);
if (!this.cursorPositionListener) {
this.cursorPositionListener = this.editor.onDidChangeCursorSelection(event =>
this.props.onCursorPositionChange!(event.selection)
);
}
}
}
private unregisterCursorListener() {
if (this.cursorPositionListener) {
this.cursorPositionListener.dispose();
this.cursorPositionListener = undefined;
}
}
/**
* Toggle editor options based on if the editor is in active state (i.e. focused).
* When the editor is not active, we want to deactivate some of the visual noise.
* @param isActive Whether editor is active.
*/
private toggleEditorOptions(isActive: boolean) {
if (this.editor) {
this.editor.updateOptions({
matchBrackets: isActive,
occurrencesHighlight: isActive,
renderIndentGuides: isActive
});
}
}
/**
* Register language features for target language. Call before setting language type to model.
*/
private registerCompletionProvider() {
const { enableCompletion, language, onRegisterCompletionProvider, shouldRegisterDefaultCompletion } = this.props;
if (enableCompletion && language) {
if (onRegisterCompletionProvider) {
onRegisterCompletionProvider(language);
} else if (shouldRegisterDefaultCompletion) {
this.registerDefaultCompletionProvider(language);
}
}
}
}
export default connect<IMonacoStateProps, void, IMonacoProps, AppState>(makeMapStateToProps)(MonacoEditor);

View File

@@ -0,0 +1,239 @@
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
// import * as monaco from "../monaco";
import { Observable, Observer } from "rxjs";
import { first, map } from "rxjs/operators";
import { childOf, JupyterMessage, ofMessageType, Channels } from "@nteract/messaging";
/**
* TODO: import from nteract when the changes under editor-base.ts are ported to nteract.
*/
import { CompletionResults, CompletionMatch, completionRequest, js_idx_to_char_idx } from "../editor-base";
/**
* Jupyter to Monaco completion item kinds.
*/
const unknownJupyterKind = "<unknown>";
const jupyterToMonacoCompletionItemKind = {
[unknownJupyterKind]: monaco.languages.CompletionItemKind.Field,
class: monaco.languages.CompletionItemKind.Class,
function: monaco.languages.CompletionItemKind.Function,
keyword: monaco.languages.CompletionItemKind.Keyword,
instance: monaco.languages.CompletionItemKind.Variable,
statement: monaco.languages.CompletionItemKind.Variable
};
/**
* Completion item provider.
*/
class CompletionItemProvider implements monaco.languages.CompletionItemProvider {
private channels: Channels | undefined;
/**
* Set Channels of Jupyter kernel.
* @param channels Channels of Jupyter kernel.
*/
setChannels(channels: Channels | undefined) {
this.channels = channels;
}
/**
* Whether provider is connected to Jupyter kernel.
*/
get isConnectedToKernel() {
return !!this.channels;
}
/**
* Additional characters to trigger completion other than Ctrl+Space.
*/
get triggerCharacters() {
return [" ", "<", "/", ".", "="];
}
/**
* Get list of completion items at position of cursor.
* @param model Monaco editor text model.
* @param position Position of cursor.
*/
async provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
// Convert to zero-based index
let cursorPos = model.getOffsetAt(position);
const code = model.getValue();
cursorPos = js_idx_to_char_idx(cursorPos, code);
// Get completions from Jupyter kernel if its Channels is connected
let items = [];
if (this.channels) {
try {
const message = completionRequest(code, cursorPos);
items = await this.codeCompleteObservable(this.channels, message, model).toPromise();
} catch (error) {
// Temporary log error to console until we settle on how we log in V3
// tslint:disable-next-line
console.error(error);
}
}
return Promise.resolve<monaco.languages.CompletionList>({
suggestions: items,
incomplete: false
});
}
/**
* Get list of completion items from Jupyter kernel.
* @param channels Channels of Jupyter kernel.
* @param message Jupyter message for completion request.
* @param model Text model.
*/
private codeCompleteObservable(channels: Channels, message: JupyterMessage, model: monaco.editor.ITextModel) {
// Process completion response
const completion$ = channels.pipe(
childOf(message),
ofMessageType("complete_reply"),
map(entry => entry.content),
first(),
map(results => this.adaptToMonacoCompletions(results, model))
);
// Subscribe and send completion request message
return Observable.create((observer: Observer<unknown>) => {
const subscription = completion$.subscribe(observer);
channels.next(message);
return subscription;
});
}
/**
* Converts Jupyter completion result to list of Monaco completion items.
*/
private adaptToMonacoCompletions(results: CompletionResults, model: monaco.editor.ITextModel) {
let range: monaco.IRange;
let percentCount = 0;
let matches = results ? results.matches : [];
if (results.metadata && results.metadata._jupyter_types_experimental) {
matches = results.metadata._jupyter_types_experimental as CompletionMatch[];
}
return matches.map((match: CompletionMatch, index: number) => {
if (typeof match === "string") {
const text = this.sanitizeText(match);
const filtered = this.getFilterText(text);
return {
kind: this.adaptToMonacoCompletionItemKind(unknownJupyterKind),
label: text,
insertText: text,
filterText: filtered,
sortText: this.getSortText(index)
} as monaco.languages.CompletionItem;
} else {
// We only need to get the range once as the range is the same for all completion items in the list.
if (!range) {
const start = model.getPositionAt(match.start);
const end = model.getPositionAt(match.end);
range = {
startLineNumber: start.lineNumber,
startColumn: start.column,
endLineNumber: end.lineNumber,
endColumn: end.column
};
// Get the range representing the text before the completion action was invoked.
// If the text starts with magics % indicator, we need to track how many of these indicators exist
// so that we ensure the insertion text only inserts the delta between what the user typed versus
// what is recommended by the completion. Without this, there will be extra % insertions.
// Example:
// User types %%p then suggestion list will recommend %%python, if we now commit the item then the
// final text in the editor becomes %%p%%python instead of %%python. This is why the tracking code
// below is needed. This behavior is only specific to the magics % indicators as Monaco does not
// handle % characters in their completion list well.
const rangeText = model.getValueInRange(range);
if (rangeText.startsWith("%%")) {
percentCount = 2;
} else if (rangeText.startsWith("%")) {
percentCount = 1;
}
}
const text = this.sanitizeText(match.text);
const filtered = this.getFilterText(text);
const insert = this.getInsertText(text, percentCount);
return {
kind: this.adaptToMonacoCompletionItemKind(match.type as keyof typeof jupyterToMonacoCompletionItemKind),
label: text,
insertText: percentCount > 0 ? insert : text,
filterText: filtered,
sortText: this.getSortText(index)
} as monaco.languages.CompletionItem;
}
});
}
/**
* Converts Jupyter completion item kind to Monaco completion item kind.
* @param kind Jupyter completion item kind.
*/
private adaptToMonacoCompletionItemKind(kind: keyof typeof jupyterToMonacoCompletionItemKind) {
const result = jupyterToMonacoCompletionItemKind[kind];
return result ? result : jupyterToMonacoCompletionItemKind[unknownJupyterKind];
}
/**
* Remove everything before a dot. Jupyter completion results like to include all characters before
* the trigger character. For example, if user types "myarray.", we expect the completion results to
* show "append", "pop", etc. but for the actual case, it will show "myarray.append", "myarray.pop",
* etc. so we are going to sanitize the text.
* @param text Text of Jupyter completion item
*/
private sanitizeText(text: string) {
const index = text.lastIndexOf(".");
return index > -1 && index < text.length - 1 ? text.substring(index + 1) : text;
}
/**
* Remove magics all % characters as Monaco doesn't like them for the filtering text.
* Without this, completion won't show magics match items.
* @param text Text of Jupyter completion item.
*/
private getFilterText(text: string) {
return text.replace(/%/g, "");
}
/**
* Get insertion text handling what to insert for the magics case depending on what
* has already been typed.
* @param text Text of Jupyter completion item.
* @param percentCount Number of percent characters to remove
*/
private getInsertText(text: string, percentCount: number) {
for (let i = 0; i < percentCount; i++) {
text = text.replace("%", "");
}
return text;
}
/**
* Maps numbers to strings, such that if a>b numerically, f(a)>f(b) lexicograhically.
* 1 -> "za", 26 -> "zz", 27 -> "zza", 28 -> "zzb", 52 -> "zzz", 53 ->"zzza"
* @param order Number to be converted to a sorting-string. order >= 0.
* @returns A string representing the order.
*/
private getSortText(order: number): string {
order++;
const numCharacters = 26; // "z" - "a" + 1;
const div = Math.floor(order / numCharacters);
let sortText = "z";
for (let i = 0; i < div; i++) {
sortText += "z";
}
const remainder = order % numCharacters;
if (remainder > 0) {
sortText += String.fromCharCode(96 + remainder);
}
return sortText;
}
}
const completionProvider = new CompletionItemProvider();
export { completionProvider };

View File

@@ -0,0 +1,44 @@
import Immutable from "immutable";
import * as monaco from "./monaco";
/**
* Code Mirror to Monaco constants.
*/
export enum Mode {
markdown = "markdown",
raw = "plaintext",
python = "python",
csharp = "csharp"
}
/**
* Maps Code Mirror mode to a valid Monaco Editor supported langauge
* defaults to plaintext if map not found.
* @param mode Code Mirror mode
* @returns Monaco language
*/
export function mapCodeMirrorModeToMonaco(mode: string | { name: string }): string {
let language = "";
// Parse codemirror mode object
if (typeof mode === "string") {
language = mode;
}
// Vanilla object
else if (typeof mode === "object" && mode.name) {
language = mode.name;
}
// Immutable Map
else if (Immutable.Map.isMap(mode) && mode.has("name")) {
language = mode.get("name");
}
// Need to handle "ipython" as a special case since it is not a registered language
if (language === "ipython") {
return Mode.python;
} else if (language === "text/x-csharp") {
return Mode.csharp;
} else if (monaco.languages.getEncodedLanguageId(language) > 0) {
return language;
}
return Mode.raw;
}

View File

@@ -0,0 +1,76 @@
// Disable linting on file since we will be moving the code below to nteract which have different rules configured.
// tslint:disable:variable-name
// tslint:disable:interface-name
/**
* TODO: Create new editor-base package in nteract repo and move all code below to new package.
*/
import { createMessage } from "@nteract/messaging";
/**
* Jupyter messaging protocol's _jupyter_types_experimental completion result.
*/
interface CompletionResult {
end: number;
start: number;
type: string;
text: string;
displayText?: string;
}
/**
* Juptyer completion match item.
*/
export type CompletionMatch = string | CompletionResult;
/**
* Jupyter messaging protocol's complete_reply response.
*/
export interface CompletionResults {
status: string;
cursor_start: number;
cursor_end: number;
matches: CompletionMatch[];
metadata?: {
_jupyter_types_experimental?: unknown;
};
}
/**
* Create Jupyter messaging protocol's complete_request message.
* @param code Code of editor.
* @param cursorPos cursor position represented in the Jupyter messaging protocol (character position)
*/
export const completionRequest = (code: string, cursorPos: number) =>
createMessage("complete_request", {
content: {
code,
cursor_pos: cursorPos
}
});
/**
* JavaScript stores text as utf16 and string indices use "code units",
* which stores high-codepoint characters as "surrogate pairs",
* which occupy two indices in the JavaScript string.
* We need to translate cursor_pos in the protocol (in characters)
* to js offset (with surrogate pairs taking two spots).
* @param js_idx JavaScript index
* @param text Text
*/
export const js_idx_to_char_idx: (js_idx: number, text: string) => number = (js_idx: number, text: string): number => {
let char_idx: number = js_idx;
for (let i = 0; i + 1 < text.length && i < js_idx; i++) {
const char_code: number = text.charCodeAt(i);
// check for surrogate pair
if (char_code >= 0xd800 && char_code <= 0xdbff) {
const next_char_code: number = text.charCodeAt(i + 1);
if (next_char_code >= 0xdc00 && next_char_code <= 0xdfff) {
char_idx--;
i++;
}
}
}
return char_idx;
};

View File

@@ -0,0 +1,10 @@
export * from "monaco-editor/esm/vs/editor/editor.api";
// /**
// * Set the custom worker url to workaround the cross-domain issue with creating web worker
// * See https://github.com/microsoft/monaco-editor/blob/master/docs/integrate-amd-cross.md for more details
// * This step has to be executed after a importing of monaco-editor once per chunk to make sure
// * the custom worker url overwrites the one from monaco-editor module itself.
// */
// import { setMonacoWorkerUrl } from "./workerUrl";
// setMonacoWorkerUrl();

View File

@@ -0,0 +1,67 @@
import { AppState, ContentRef, selectors as nteractSelectors } from "@nteract/core";
import { CellId } from "@nteract/commutable";
import { Mode, mapCodeMirrorModeToMonaco } from "./converter";
/**
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given cell, falling back to the notebook language if one for the cell is not defined.
*/
export const getCellMonacoLanguage = (
state: AppState,
contentRef: ContentRef,
cellId: CellId,
cellLanguageOverride?: string,
notebookLanguageOverride?: string
): string => {
const model = nteractSelectors.model(state, { contentRef });
if (!model || model.type !== "notebook") {
throw new Error("Connected Editor components should not be used with non-notebook models");
}
const cell = nteractSelectors.notebook.cellById(model, { id: cellId });
if (!cell) {
throw new Error("Invalid cell id");
}
switch (cell.cell_type) {
case "markdown":
return Mode.markdown;
case "raw":
return Mode.raw;
case "code":
if (cellLanguageOverride) {
return mapCodeMirrorModeToMonaco(cellLanguageOverride);
} else {
// Fall back to notebook language if cell language isn't present.
return getNotebookMonacoLanguage(state, contentRef, notebookLanguageOverride);
}
}
};
/**
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given notebook.
*/
export const getNotebookMonacoLanguage = (
state: AppState,
contentRef: ContentRef,
notebookLanguageOverride?: string
): string => {
const model = nteractSelectors.model(state, { contentRef });
if (!model || model.type !== "notebook") {
throw new Error("Connected Editor components should not be used with non-notebook models");
}
if (notebookLanguageOverride) {
return mapCodeMirrorModeToMonaco(notebookLanguageOverride);
}
const kernelRef = model.kernelRef;
let codeMirrorMode;
// Try to get the CodeMirror mode from the kernel.
if (kernelRef) {
codeMirrorMode = nteractSelectors.kernel(state, { kernelRef })?.info?.codemirrorMode;
}
// As a fallback, get the CodeMirror mode from the notebook itself.
codeMirrorMode = codeMirrorMode ?? nteractSelectors.notebook.codeMirrorMode(model);
return mapCodeMirrorModeToMonaco(codeMirrorMode);
};

View File

@@ -0,0 +1,22 @@
/*
For the following components, we use the inherited width values from monaco-container.
On resizing the browser, the width of monaco-container will be calculated
and we just use the calculated width for the following components
So we don't need to use Monaco editor's layout() function which is expensive operation and causes performance issues on resizing.
*/
/*
TODO: These styles below are added for resizing perf improvement.
Once the virtualization is implemented, we will revisit this later.
*/
.monaco-container .monaco-editor {
width: inherit !important;
}
.monaco-container .monaco-editor .overflow-guard {
width: inherit !important;
}
/* 26px is the left margin for .monaco-scrollable-element */
.monaco-container .monaco-editor .monaco-scrollable-element.editor-scrollable.vs {
width: calc(100% - 26px) !important;
}

View File

@@ -0,0 +1,75 @@
import * as monaco from "./monaco";
// TODO: move defineTheme calls to an initialization function
/**
* The default light theme with customized background
*/
export const LightThemeName = "vs-light";
/**
* Default monaco theme for light theme
*/
export const customMonacoLightTheme: monaco.editor.IStandaloneThemeData = {
base: "vs", // Derive from default light theme of Monaco
inherit: true,
rules: [],
colors: {
// We want Monaco background to use the same background for our themes.
// Without this, the Monaco light theme has a yellowish tone.
// Verified with UX that white meets all the accessbility requirements for light
// and high contrast light theme.
"editor.background": "#FFFFFF"
}
};
monaco.editor.defineTheme(LightThemeName, customMonacoLightTheme);
/**
* The default dark theme with customized background
*/
export const DarkThemeName = "aznb-dark";
/**
* Default monaco theme for dark theme
*/
export const customMonacoDarkTheme: monaco.editor.IStandaloneThemeData = {
base: "vs-dark", // Derive from default dark theme of Monaco
inherit: true,
rules: [],
colors: {
"editor.background": "#1b1a19"
}
};
monaco.editor.defineTheme(DarkThemeName, customMonacoDarkTheme);
/**
* The custom high contrast light theme with customized background
*/
export const HCLightThemeName = "hc-light";
/**
* Default monaco theme for light high contrast mode
*/
export const customMonacoHCLightTheme: monaco.editor.IStandaloneThemeData = {
base: "vs", // Derive from default light theme of Monaco; change all grey colors to black to comply with highcontrast rules
inherit: true,
rules: [
{ token: "annotation", foreground: "000000" },
{ token: "delimiter.html", foreground: "000000" },
{ token: "operator.scss", foreground: "000000" },
{ token: "operator.sql", foreground: "000000" },
{ token: "operator.swift", foreground: "000000" },
{ token: "predefined.sql", foreground: "000000" }
],
colors: {
// We want Monaco background to use the same background for our themes.
// Without this, the Monaco light theme has a yellowish tone.
// Verified with UX that white meets all the accessbility requirements for light
// and high contrast light theme.
"editor.background": "#FFFFFF"
}
};
monaco.editor.defineTheme(HCLightThemeName, customMonacoHCLightTheme);

View File

@@ -98,7 +98,7 @@ export class NotebookComponentBootstrapper {
actions.fetchContentFulfilled({
filepath: undefined,
model: NotebookComponentBootstrapper.wrapModelIntoContent(name, undefined, content),
kernelRef: createKernelRef(),
kernelRef: undefined, // must be undefined or it will be auto-started by the epic
contentRef: this.contentRef
})
);

View File

@@ -1,13 +1,13 @@
import * as Immutable from "immutable";
import { ActionsObservable, StateObservable } from "redux-observable";
import { Subject } from "rxjs";
import { Subject, empty } from "rxjs";
import { toArray } from "rxjs/operators";
import { makeNotebookRecord } from "@nteract/commutable";
import { actions, state } from "@nteract/core";
import * as sinon from "sinon";
import { CdbAppState, makeCdbRecord } from "./types";
import { launchWebSocketKernelEpic } from "./epics";
import { launchWebSocketKernelEpic, autoStartKernelEpic } from "./epics";
import { NotebookUtil } from "../NotebookUtil";
import { sessions } from "rx-jupyter";
@@ -74,46 +74,47 @@ describe("Extract kernel from notebook", () => {
});
});
const initialState = {
app: state.makeAppRecord({
host: state.makeJupyterHostRecord({
type: "jupyter",
token: "eh",
basePath: "/"
})
}),
comms: state.makeCommsRecord(),
config: Immutable.Map({}),
core: state.makeStateRecord({
kernelRef: "fake",
entities: state.makeEntitiesRecord({
contents: state.makeContentsRecord({
byRef: Immutable.Map({
fakeContentRef: state.makeNotebookContentRecord()
})
}),
kernels: state.makeKernelsRecord({
byRef: Immutable.Map({
fake: state.makeRemoteKernelRecord({
type: "websocket",
channels: new Subject<any>(),
kernelSpecName: "fancy",
id: "0"
})
})
})
})
}),
cdb: makeCdbRecord({
databaseAccountName: "dbAccountName",
defaultExperience: "defaultExperience"
})
};
describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef";
const kernelRef = "fake";
const initialState = {
app: state.makeAppRecord({
host: state.makeJupyterHostRecord({
type: "jupyter",
token: "eh",
basePath: "/"
})
}),
comms: state.makeCommsRecord(),
config: Immutable.Map({}),
core: state.makeStateRecord({
kernelRef: "fake",
entities: state.makeEntitiesRecord({
contents: state.makeContentsRecord({
byRef: Immutable.Map({
fakeContentRef: state.makeNotebookContentRecord()
})
}),
kernels: state.makeKernelsRecord({
byRef: Immutable.Map({
fake: state.makeRemoteKernelRecord({
type: "websocket",
channels: new Subject<any>(),
kernelSpecName: "fancy",
id: "0"
})
})
})
})
}),
cdb: makeCdbRecord({
databaseAccountName: "dbAccountName",
defaultExperience: "defaultExperience"
})
};
it("launches remote kernels", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
@@ -490,3 +491,55 @@ describe("launchWebSocketKernelEpic", () => {
});
});
});
describe("autoStartKernelEpic", () => {
const contentRef = "fakeContentRef";
const kernelRef = "fake";
it("automatically starts kernel when content fetch is successful if kernelRef is defined", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.fetchContentFulfilled({
contentRef,
kernelRef,
filepath: "filepath",
model: {}
})
);
const responseActions = await autoStartKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(responseActions).toMatchObject([
{
type: actions.RESTART_KERNEL,
payload: {
contentRef,
kernelRef,
outputHandling: "None"
}
}
]);
});
it("Don't start kernel when content fetch is successful if kernelRef is not defined", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.fetchContentFulfilled({
contentRef,
kernelRef: undefined,
filepath: "filepath",
model: {}
})
);
const responseActions = await autoStartKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(responseActions).toMatchObject([]);
});
});

View File

@@ -1,4 +1,4 @@
import { empty, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { webSocket } from "rxjs/webSocket";
import { ActionsObservable, StateObservable } from "redux-observable";
import { ofType } from "redux-observable";
@@ -77,7 +77,7 @@ const addInitialCodeCellEpic = (
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return empty();
return EMPTY;
}
const cellOrder = selectors.notebook.cellOrder(model);
@@ -90,7 +90,40 @@ const addInitialCodeCellEpic = (
);
}
return empty();
return EMPTY;
})
);
};
/**
* Automatically start kernel if kernelRef is present.
* The kernel is normally lazy-started when a cell is being executed, but a running kernel is
* required for code completion to work.
* For notebook viewer, there is no kernel
* @param action$
* @param state$
*/
export const autoStartKernelEpic = (
action$: ActionsObservable<actions.FetchContentFulfilled>,
state$: StateObservable<AppState>
): Observable<{} | actions.CreateCellBelow> => {
return action$.pipe(
ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap(action => {
const state = state$.value;
const { contentRef, kernelRef } = action.payload;
if (!kernelRef) {
return EMPTY;
}
return of(
actions.restartKernel({
contentRef,
kernelRef,
outputHandling: "None"
})
);
})
);
};
@@ -288,7 +321,7 @@ export const launchWebSocketKernelEpic = (
const state = state$.value;
const host = selectors.currentHost(state);
if (host.type !== "jupyter") {
return empty();
return EMPTY;
}
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
serverConfig.userPuid = getUserPuid();
@@ -299,7 +332,7 @@ export const launchWebSocketKernelEpic = (
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") {
return empty();
return EMPTY;
}
let kernelSpecToLaunch = kernelSpecName;
@@ -513,26 +546,26 @@ const changeWebSocketKernelEpic = (
const state = state$.value;
const host = selectors.currentHost(state);
if (host.type !== "jupyter") {
return empty();
return EMPTY;
}
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
if (!oldKernelRef) {
return empty();
return EMPTY;
}
const oldKernel = selectors.kernel(state, { kernelRef: oldKernelRef });
if (!oldKernel || oldKernel.type !== "websocket") {
return empty();
return EMPTY;
}
const { sessionId } = oldKernel;
if (!sessionId) {
return empty();
return EMPTY;
}
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") {
return empty();
return EMPTY;
}
const {
filepath,
@@ -593,7 +626,7 @@ const focusInitialCodeCellEpic = (
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return empty();
return EMPTY;
}
const cellOrder = selectors.notebook.cellOrder(model);
@@ -608,7 +641,7 @@ const focusInitialCodeCellEpic = (
);
}
return empty();
return EMPTY;
})
);
};
@@ -661,7 +694,7 @@ const notificationsToUserEpic = (
break;
}
}
return empty();
return EMPTY;
})
);
};
@@ -701,7 +734,7 @@ const handleKernelConnectionLostEpic = (
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
return of(empty());
return of(EMPTY);
}
return concat(
@@ -814,7 +847,7 @@ const closeUnsupportedMimetypesEpic = (
explorer.showOkModalDialog("File cannot be rendered", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
}
return empty();
return EMPTY;
})
);
};
@@ -842,13 +875,14 @@ const closeContentFailedToFetchEpic = (
explorer.showOkModalDialog("Failure to load", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
}
return empty();
return EMPTY;
})
);
};
export const allEpics = [
addInitialCodeCellEpic,
autoStartKernelEpic,
focusInitialCodeCellEpic,
notificationsToUserEpic,
launchWebSocketKernelEpic,

View File

@@ -30,6 +30,7 @@ import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler";
import MonacoEditor from "../MonacoEditor/MonacoEditor";
import * as cdbActions from "../NotebookComponent/actions";
export interface NotebookRendererBaseProps {
@@ -116,7 +117,12 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
{{
editor: {
codemirror: (props: PassedEditorProps) => (
<CodeMirrorEditor {...props} lineNumbers={true} />
<MonacoEditor
{...props}
lineNumbers={true}
enableCompletion={true}
shouldRegisterDefaultCompletion={true}
/>
)
},
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (

View File

@@ -40,7 +40,7 @@ describe("Add Collection Pane", () => {
};
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});

View File

@@ -329,7 +329,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isEmulator &&
!this.container.isTryCosmosDBSubscription() &&
this.container.getPlatformType() !== PlatformType.Portal
) {
@@ -341,7 +341,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.maxCollectionsReached = ko.computed<boolean>(() => {

View File

@@ -40,7 +40,10 @@ describe("Add Database Pane", () => {
};
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({
notificationsClient: null,
isEmulator: false
});
});
it("should be true if subscription type is Benefits", () => {

View File

@@ -14,10 +14,9 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { PlatformType } from "../../PlatformType";
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext";
import { configContext, Platform } from "../../ConfigContext";
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
@@ -180,7 +179,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isEmulator &&
!this.container.isTryCosmosDBSubscription() &&
this.container.getPlatformType() !== PlatformType.Portal
) {
@@ -203,7 +202,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
@@ -305,76 +304,23 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.formErrors("");
this.isExecuting(true);
const createDatabaseParameters: DataModels.RpParameters = {
db: addDatabasePaneStartMessage.database.id,
st: addDatabasePaneStartMessage.database.shared,
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: addDatabasePaneStartMessage.databaseAccountName
const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(),
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
offerThroughput: addDatabasePaneStartMessage.offerThroughput
};
const autopilotSettings = this._getAutopilotSettings();
if (this.container.isPreferredApiCassandra()) {
this._createKeyspace(createDatabaseParameters, autopilotSettings, startKey);
} else if (this.container.isPreferredApiMongoDB() && EnvironmentUtility.isAadUser()) {
this._createMongoDatabase(createDatabaseParameters, autopilotSettings, startKey);
} else if (this.container.isPreferredApiGraph() && EnvironmentUtility.isAadUser()) {
this._createGremlinDatabase(createDatabaseParameters, autopilotSettings, startKey);
} else if (this.container.isPreferredApiDocumentDB() && EnvironmentUtility.isAadUser()) {
this._createSqlDatabase(createDatabaseParameters, autopilotSettings, startKey);
} else {
this._createDatabase(offerThroughput, startKey);
}
}
private _createSqlDatabase(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
) {
AddDbUtilities.createSqlDatabase(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings).then(
() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
createDatabase(createDatabaseParams).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, startKey);
},
(reason: any) => {
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
}
);
}
private _createMongoDatabase(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
) {
AddDbUtilities.createMongoDatabaseWithARM(
this.container.armEndpoint(),
createDatabaseParameters,
autoPilotSettings
).then(() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
});
}
private _createGremlinDatabase(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
) {
AddDbUtilities.createGremlinDatabase(
this.container.armEndpoint(),
createDatabaseParameters,
autoPilotSettings
).then(() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
});
}
public resetData() {
this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault());
@@ -397,72 +343,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
return true;
}
private _createDatabase(offerThroughput: number, telemetryStartKey: number): void {
const autoPilot: DataModels.AutoPilotCreationSettings = this._isAutoPilotSelectedAndWhatTier();
const createRequest: DataModels.CreateDatabaseRequest = {
databaseId: this.databaseId().trim(),
offerThroughput,
databaseLevelThroughput: this.databaseCreateNewShared(),
autoPilot,
hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag()
};
createDatabase(createRequest).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
},
(reason: any) => {
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
}
);
}
private _createKeyspace(
createDatabaseParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
): void {
if (EnvironmentUtility.isAadUser()) {
this._createKeyspaceUsingRP(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings, startKey);
} else {
this._createKeyspaceUsingProxy(createDatabaseParameters.offerThroughput, startKey);
}
}
private _createKeyspaceUsingProxy(offerThroughput: number, telemetryStartKey: number): void {
const provisionThroughputQueryPart: string = this.databaseCreateNewShared()
? `AND cosmosdb_provisioned_throughput=${offerThroughput}`
: "";
const createKeyspaceQuery: string = `CREATE KEYSPACE ${this.databaseId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 } ${provisionThroughputQueryPart};`;
(this.container.tableDataClient as CassandraAPIDataClient)
.createKeyspace(
this.container.databaseAccount().properties.cassandraEndpoint,
this.container.databaseAccount().id,
this.container,
createKeyspaceQuery
)
.then(
() => {
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
},
(reason: any) => {
this._onCreateDatabaseFailure(reason, offerThroughput, telemetryStartKey);
}
);
}
private _createKeyspaceUsingRP(
armEndpoint: string,
createKeyspaceParameters: DataModels.RpParameters,
autoPilotSettings: DataModels.RpOptions,
startKey: number
): void {
AddDbUtilities.createCassandraKeyspace(armEndpoint, createKeyspaceParameters, autoPilotSettings).then(() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createKeyspaceParameters.offerThroughput, startKey);
});
});
}
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
this.isExecuting(false);
this.close();
@@ -583,20 +463,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
return undefined;
}
private _getAutopilotSettings(): DataModels.RpOptions {
if (
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) ||
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
[Constants.HttpHeaders.autoPilotThroughput]: { maxThroughput: this.maxAutoPilotThroughputSet() * 1 }
}
: { [Constants.HttpHeaders.autoPilotTier]: this.selectedAutoPilotTier().toString() };
}
return undefined;
}
private _updateThroughputLimitByDatabase() {
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.throughput(throughputDefaults.shared);

View File

@@ -12,7 +12,6 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>;
@@ -232,11 +231,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
if (!this.container.isEmulator && !this.container.isTryCosmosDBSubscription()) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}

View File

@@ -17,7 +17,7 @@ describe("Delete Collection Confirmation Pane", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be true if 1 database and 1 collection", () => {
@@ -56,7 +56,7 @@ describe("Delete Collection Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => {
it("should return true if last collection and database does not have shared throughput else false", () => {
let fakeExplorer = new Explorer();
let fakeExplorer = new Explorer({ notificationsClient: null, isEmulator: false });
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
fakeExplorer.refreshAllDatabases = () => Q.resolve();

View File

@@ -22,7 +22,7 @@ describe("Delete Database Confirmation Pane", () => {
});
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be true if only 1 database", () => {

View File

@@ -16,7 +16,7 @@ export interface GenericRightPaneProps {
onSubmit: () => void;
submitButtonText: string;
title: string;
isSubmitButtonVisible?: boolean;
isSubmitButtonHidden?: boolean;
}
export interface GenericRightPaneState {
@@ -108,7 +108,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
style={{ visibility: this.props.isSubmitButtonVisible ? "visible" : "hidden" }}
style={{ visibility: this.props.isSubmitButtonHidden ? "hidden" : "visible" }}
ariaLabel="Submit"
title="Submit"
onClick={this.props.onSubmit}

View File

@@ -52,7 +52,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
submitButtonText: "Publish",
onClose: () => this.close(),
onSubmit: () => this.submit(),
isSubmitButtonVisible: this.isCodeOfConductAccepted
isSubmitButtonHidden: !this.isCodeOfConductAccepted
};
const publishNotebookPaneProps: PublishNotebookPaneProps = {

View File

@@ -285,7 +285,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
<GalleryCardComponent
data={{
id: undefined,
name: this.props.notebookName,
name: this.state.notebookName,
description: this.state.notebookDescription,
gitSha: undefined,
tags: this.state.notebookTags.split(","),

View File

@@ -7,7 +7,7 @@ describe("Settings Pane", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be true for SQL API", () => {

View File

@@ -39,7 +39,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
formErrorDetail: this.formErrorDetail,
id: "uploaditemspane",
isExecuting: this.isExecuting,
isSubmitButtonVisible: true,
title: "Upload Items",
submitButtonText: "Upload",
onClose: () => this.close(),

View File

@@ -6,7 +6,7 @@ import Explorer from "../Explorer";
jest.mock("../Explorer");
const createExplorer = () => {
const mock = new Explorer();
const mock = new Explorer({} as any);
mock.selectedNode = ko.observable();
mock.isNotebookEnabled = ko.observable(false);
mock.addCollectionText = ko.observable("add collection");

View File

@@ -20,7 +20,6 @@ import { updateOffer } from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { userContext } from "../../UserContext";
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
import { configContext, Platform } from "../../ConfigContext";
const updateThroughputBeyondLimitWarningMessage: string = `
You are about to request an increase in throughput beyond the pre-allocated capacity.
@@ -197,7 +196,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
});
this.costsVisible = ko.computed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(
@@ -208,7 +207,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
);
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform === Platform.Emulator ||
!!this.container.isEmulator ||
this.container.getPlatformType() === PlatformType.Hosted ||
this.canThroughputExceedMaximumValue()
) {

View File

@@ -27,9 +27,15 @@ describe("Documents tab", () => {
});
describe("showPartitionKey", () => {
const explorer = new Explorer();
const explorer = new Explorer({
notificationsClient: null,
isEmulator: false
});
const mongoExplorer = new Explorer();
const mongoExplorer = new Explorer({
notificationsClient: null,
isEmulator: false
});
mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{

View File

@@ -147,6 +147,30 @@ export default class NotebookTabV2 extends TabsBase {
const cellCodeType = "code";
const cellMarkdownType = "markdown";
const cellRawType = "raw";
const saveButtonChildren = [];
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
saveButtonChildren.push({
iconName: "Copy",
onCommandClick: () => this.copyNotebook(),
commandButtonLabel: copyToLabel,
hasPopup: false,
disabled: false,
ariaLabel: copyToLabel
});
}
if (this.container.isGalleryPublishEnabled()) {
saveButtonChildren.push({
iconName: "PublishContent",
onCommandClick: async () => await this.publishToGallery(),
commandButtonLabel: publishLabel,
hasPopup: false,
disabled: false,
ariaLabel: publishLabel
});
}
let buttons: CommandButtonComponentProps[] = [
{
iconSrc: SaveIcon,
@@ -156,34 +180,17 @@ export default class NotebookTabV2 extends TabsBase {
hasPopup: false,
disabled: false,
ariaLabel: saveLabel,
children: this.container.isGalleryPublishEnabled()
? [
{
iconName: "Save",
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
commandButtonLabel: saveLabel,
hasPopup: false,
disabled: false,
ariaLabel: saveLabel
},
{
iconName: "Copy",
onCommandClick: () => this.copyNotebook(),
commandButtonLabel: copyToLabel,
hasPopup: false,
disabled: false,
ariaLabel: copyToLabel
},
{
iconName: "PublishContent",
onCommandClick: async () => await this.publishToGallery(),
commandButtonLabel: publishLabel,
hasPopup: false,
disabled: false,
ariaLabel: publishLabel
}
]
: undefined
children: saveButtonChildren.length && [
{
iconName: "Save",
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
commandButtonLabel: saveLabel,
hasPopup: false,
disabled: false,
ariaLabel: saveLabel
},
...saveButtonChildren
]
},
{
iconSrc: null,

View File

@@ -49,7 +49,7 @@ describe("Query Tab", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be true for accounts using SQL API", () => {
@@ -69,7 +69,7 @@ describe("Query Tab", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be visible when using a supported API", () => {

View File

@@ -7,6 +7,7 @@ import Database from "../Tree/Database";
import Explorer from "../Explorer";
import SettingsTab from "../Tabs/SettingsTab";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { IndexingPolicies } from "../../Shared/Constants";
describe("Settings tab", () => {
const baseCollection: DataModels.Collection = {
@@ -16,7 +17,7 @@ describe("Settings tab", () => {
mode: DataModels.ConflictResolutionMode.LastWriterWins,
conflictResolutionPath: "/_ts"
},
indexingPolicy: {},
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
_rid: "",
_self: "",
_etag: "",
@@ -51,7 +52,7 @@ describe("Settings tab", () => {
defaultTtl: 200,
partitionKey: null,
conflictResolutionPolicy: null,
indexingPolicy: {},
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
_rid: "",
_self: "",
_etag: "",
@@ -78,7 +79,7 @@ describe("Settings tab", () => {
};
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});
@@ -177,7 +178,7 @@ describe("Settings tab", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});
@@ -255,7 +256,7 @@ describe("Settings tab", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});
@@ -336,13 +337,15 @@ describe("Settings tab", () => {
}
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
const explorer = new Explorer();
const explorer = new Explorer({
notificationsClient: null,
isEmulator: false
});
explorer.defaultExperience(defaultApi);
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
const offer: DataModels.Offer = null;
const defaultTtl = 200;
const indexingPolicy = {};
const database = new Database(explorer, baseDatabase, null);
const conflictResolutionPolicy = {
mode: DataModels.ConflictResolutionMode.LastWriterWins,
@@ -364,7 +367,7 @@ describe("Settings tab", () => {
}
: null,
conflictResolutionPolicy: conflictResolutionPolicy,
indexingPolicy: indexingPolicy,
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
_rid: "",
_self: "",
_etag: "",
@@ -468,7 +471,10 @@ describe("Settings tab", () => {
describe("AutoPilot", () => {
function getCollection(autoPilotTier: DataModels.AutopilotTier) {
const explorer = new Explorer();
const explorer = new Explorer({
notificationsClient: null,
isEmulator: false
});
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
explorer.databaseAccount({

View File

@@ -17,12 +17,11 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { PlatformType } from "../../PlatformType";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../Explorer";
import { updateOffer, updateCollection } from "../../Common/DocumentClientUtilityBase";
import { updateOffer } from "../../Common/DocumentClientUtilityBase";
import { updateCollection } from "../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { userContext } from "../../UserContext";
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
import { config } from "process";
import { configContext, Platform } from "../../ConfigContext";
const ttlWarning: string = `
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application.
@@ -456,7 +455,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
});
this.rupmVisible = ko.computed(() => {
if (configContext.platform === Platform.Emulator) {
if (this.container.isEmulator) {
return false;
}
if (this.container.isFeatureEnabled(Constants.Features.enableRupm)) {
@@ -486,7 +485,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
});
this.costsVisible = ko.computed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.isTryCosmosDBSubscription = ko.computed<boolean>(() => {
@@ -502,7 +501,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
});
this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform === Platform.Emulator) {
if (this.container.isEmulator) {
return false;
}
@@ -709,7 +708,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}
const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs();
const isEmulator = configContext.platform === Platform.Emulator;
const isEmulator = this.container.isEmulator;
if (isThroughputGreaterThanMaxRus && isEmulator) {
return false;
}
@@ -880,8 +879,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
const throughputExceedsMaxValue: boolean =
configContext.platform !== Platform.Emulator && this.throughput() > this.maxRUs();
const throughputExceedsMaxValue: boolean = !this.container.isEmulator && this.throughput() > this.maxRUs();
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
const ttlOrIndexingPolicyFieldsDirty: boolean =
@@ -1012,8 +1010,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
);
}
public onSaveClick = (): Q.Promise<any> => {
let promises: Q.Promise<void>[] = [];
public onSaveClick = async (): Promise<any> => {
this.isExecutionError(false);
this.isExecuting(true);
@@ -1026,50 +1023,60 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
const newCollectionAttributes: any = {};
if (this.shouldUpdateCollection()) {
let defaultTtl: number;
switch (this.timeToLive()) {
case "on":
defaultTtl = Number(this.timeToLiveSeconds());
break;
case "on-nodefault":
defaultTtl = -1;
break;
case "off":
default:
defaultTtl = undefined;
break;
}
try {
if (this.shouldUpdateCollection()) {
let defaultTtl: number;
switch (this.timeToLive()) {
case "on":
defaultTtl = Number(this.timeToLiveSeconds());
break;
case "on-nodefault":
defaultTtl = -1;
break;
case "off":
default:
defaultTtl = undefined;
break;
}
newCollectionAttributes.defaultTtl = defaultTtl;
newCollectionAttributes.defaultTtl = defaultTtl;
newCollectionAttributes.indexingPolicy = this.indexingPolicyContent();
newCollectionAttributes.indexingPolicy = this.indexingPolicyContent();
newCollectionAttributes.changeFeedPolicy =
this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On
? ({
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
} as DataModels.ChangeFeedPolicy)
newCollectionAttributes.changeFeedPolicy =
this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On
? ({
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
} as DataModels.ChangeFeedPolicy)
: undefined;
newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
? this.analyticalStorageTtlSelection() === "on"
? Number(this.analyticalStorageTtlSeconds())
: Constants.AnalyticalStorageTtl.Infinite
: undefined;
newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
? this.analyticalStorageTtlSelection() === "on"
? Number(this.analyticalStorageTtlSeconds())
: Constants.AnalyticalStorageTtl.Infinite
: undefined;
newCollectionAttributes.geospatialConfig = {
type: this.geospatialConfigType()
};
newCollectionAttributes.geospatialConfig = {
type: this.geospatialConfigType()
};
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
if (!!conflictResolutionChanges) {
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
}
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
if (!!conflictResolutionChanges) {
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
}
const newCollection: DataModels.Collection = _.extend(
{},
this.collection.rawDataModel,
newCollectionAttributes
);
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newCollection
);
const newCollection: DataModels.Collection = _.extend({}, this.collection.rawDataModel, newCollectionAttributes);
const updateCollectionPromise = updateCollection(this.collection.databaseId, this.collection, newCollection).then(
(updatedCollection: DataModels.Collection) => {
if (updatedCollection) {
this.collection.rawDataModel = updatedCollection;
this.collection.defaultTtl(updatedCollection.defaultTtl);
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
@@ -1079,164 +1086,133 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
}
);
promises.push(updateCollectionPromise);
}
if (
this.throughput.editableIsDirty() ||
this.rupm.editableIsDirty() ||
this._isAutoPilotDirty() ||
this._hasProvisioningTypeChanged()
) {
const newThroughput = this.throughput();
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
if (newOffer.content) {
newOffer.content.offerThroughput = newThroughput;
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
} else {
newOffer = _.extend({}, newOffer, {
content: {
offerThroughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
}
});
}
const headerOptions: RequestOptions = { initialHeaders: {} };
if (this.isAutoPilotSelected()) {
if (!this.hasAutoPilotV2FeatureFlag()) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: this.autoPilotThroughput()
};
} else {
newOffer.content.offerAutopilotSettings = {
tier: this.selectedAutoPilotTier()
};
}
// user has changed from provisioned --> autoscale
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
delete newOffer.content.offerAutopilotSettings;
} else {
delete newOffer.content.offerThroughput;
}
} else {
this.isAutoPilotSelected(false);
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
// user has changed from autoscale --> provisioned
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
} else {
delete newOffer.content.offerAutopilotSettings;
}
}
if (
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.container != null
this.throughput.editableIsDirty() ||
this.rupm.editableIsDirty() ||
this._isAutoPilotDirty() ||
this._hasProvisioningTypeChanged()
) {
const requestPayload = {
subscriptionId: userContext.subscriptionId,
databaseAccountName: userContext.databaseAccount.name,
resourceGroup: userContext.resourceGroup,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
throughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
};
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
() => {
this.collection.offer().content.offerThroughput = originalThroughputValue;
this.throughput(originalThroughputValue);
this.notificationStatusInfo(
throughputApplyDelayedMessage(
this.isAutoPilotSelected(),
originalThroughputValue,
this._getThroughputUnit(),
this.collection.databaseId,
this.collection.id(),
newThroughput
)
);
this.throughput.valueHasMutated(); // force component re-render
},
(error: any) => {
TelemetryProcessor.traceFailure(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: error
},
startKey
);
}
);
promises.push(Q(updateOfferBeyondLimitPromise));
} else {
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
(updatedOffer: DataModels.Offer) => {
this.collection.offer(updatedOffer);
this.collection.offer.valueHasMutated();
}
);
const newThroughput = this.throughput();
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
promises.push(updateOfferPromise);
}
}
if (promises.length === 0) {
this.isExecuting(false);
}
return Q.all(promises)
.then(
() => {
this.container.isRefreshingExplorer(false);
this._setBaseline();
this.collection.readSettings();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
TelemetryProcessor.traceSuccess(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
startKey
);
},
(reason: any) => {
this.container.isRefreshingExplorer(false);
this.isExecutionError(true);
console.error(reason);
TelemetryProcessor.traceFailure(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
startKey
);
if (newOffer.content) {
newOffer.content.offerThroughput = newThroughput;
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
} else {
newOffer = _.extend({}, newOffer, {
content: {
offerThroughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
}
});
}
)
.finally(() => this.isExecuting(false));
const headerOptions: RequestOptions = { initialHeaders: {} };
if (this.isAutoPilotSelected()) {
if (!this.hasAutoPilotV2FeatureFlag()) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: this.autoPilotThroughput()
};
} else {
newOffer.content.offerAutopilotSettings = {
tier: this.selectedAutoPilotTier()
};
}
// user has changed from provisioned --> autoscale
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
delete newOffer.content.offerAutopilotSettings;
} else {
delete newOffer.content.offerThroughput;
}
} else {
this.isAutoPilotSelected(false);
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
// user has changed from autoscale --> provisioned
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
} else {
delete newOffer.content.offerAutopilotSettings;
}
}
if (
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.container != null
) {
const requestPayload = {
subscriptionId: userContext.subscriptionId,
databaseAccountName: userContext.databaseAccount.name,
resourceGroup: userContext.resourceGroup,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
throughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
};
await updateOfferThroughputBeyondLimit(requestPayload);
this.collection.offer().content.offerThroughput = originalThroughputValue;
this.throughput(originalThroughputValue);
this.notificationStatusInfo(
throughputApplyDelayedMessage(
this.isAutoPilotSelected(),
originalThroughputValue,
this._getThroughputUnit(),
this.collection.databaseId,
this.collection.id(),
newThroughput
)
);
this.throughput.valueHasMutated(); // force component re-render
} else {
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
this.collection.offer(updatedOffer);
this.collection.offer.valueHasMutated();
}
}
this.container.isRefreshingExplorer(false);
this._setBaseline();
this.collection.readSettings();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
TelemetryProcessor.traceSuccess(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
startKey
);
} catch (error) {
this.container.isRefreshingExplorer(false);
this.isExecutionError(true);
console.error(error);
TelemetryProcessor.traceFailure(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: error
},
startKey
);
}
this.isExecuting(false);
};
public onRevertClick = (): Q.Promise<any> => {

View File

@@ -16,7 +16,7 @@ describe("Tabs manager tests", () => {
let documentsTab: DocumentsTab;
beforeAll(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: undefined, isEmulator: false });
explorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
id: "test",
name: "test",

View File

@@ -559,6 +559,7 @@ export default class Collection implements ViewModels.Collection {
});
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
return tab.collection && tab.collection.rid === this.rid;
});
@@ -574,8 +575,9 @@ export default class Collection implements ViewModels.Collection {
tabTitle: tabTitle
});
Q.all([this.readSettings()]).then(
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
(data: any) => {
const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings,
title: !this.offer() ? "Settings" : "Scale & Settings",
@@ -590,6 +592,7 @@ export default class Collection implements ViewModels.Collection {
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
this.container.tabsManager.activateNewTab(settingsTab);
settingsTab.pendingNotification(pendingNotification);
},
(error: any) => {
TelemetryProcessor.traceFailure(
@@ -613,7 +616,16 @@ export default class Collection implements ViewModels.Collection {
}
);
} else {
this.container.tabsManager.activateTab(settingsTab);
pendingNotificationsPromise.then(
(pendingNotification: DataModels.Notification) => {
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab);
},
(error: any) => {
settingsTab.pendingNotification(undefined);
this.container.tabsManager.activateTab(settingsTab);
}
);
}
};
@@ -636,7 +648,9 @@ export default class Collection implements ViewModels.Collection {
});
// TODO: Use the collection entity cache to get quota info
const quotaInfoPromise: Q.Promise<DataModels.CollectionQuotaInfo> = readCollectionQuotaInfo(this);
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers();
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
isServerless: this.container.isServerlessEnabled()
});
Q.all([quotaInfoPromise, offerInfoPromise]).then(
() => {
this.container.isRefreshingExplorer(false);
@@ -645,9 +659,7 @@ export default class Collection implements ViewModels.Collection {
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel);
const isDatabaseShared = this.getDatabase() && this.getDatabase().isDatabaseShared();
const isServerless = this.container.isServerlessEnabled();
if ((isDatabaseShared || isServerless) && !collectionOffer) {
if (!collectionOffer) {
this.quotaInfo(quotaInfo);
TelemetryProcessor.traceSuccess(
Action.LoadOffers,
@@ -1271,6 +1283,48 @@ export default class Collection implements ViewModels.Collection {
return deferred.promise;
}
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
if (!this.container) {
return Q.resolve(undefined);
}
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
this.container.notificationsClient.fetchNotifications().then(
(notifications: DataModels.Notification[]) => {
if (!notifications || notifications.length === 0) {
deferred.resolve(undefined);
return;
}
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
return (
notification.kind === "message" &&
notification.collectionName === this.id() &&
notification.description &&
throughputUpdateRegExp.test(notification.description)
);
});
deferred.resolve(pendingNotification);
},
(error: any) => {
Logger.logError(
JSON.stringify({
error: JSON.stringify(error),
accountName: this.container && this.container.databaseAccount(),
databaseName: this.databaseId,
collectionName: this.id()
}),
"Settings tree node"
);
deferred.resolve(undefined);
}
);
return deferred.promise;
}
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
const numFiles: number = uploadDetailsRecords.length;

View File

@@ -52,6 +52,7 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree
});
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.DatabaseSettings,
tab => tab.rid === this.rid
@@ -65,8 +66,9 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale"
});
Q.all([this.readSettings()]).then(
() => {
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
(data: any) => {
const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new DatabaseSettingsTab({
tabKind: ViewModels.CollectionTabKind.DatabaseSettings,
title: "Scale",
@@ -81,6 +83,7 @@ export default class Database implements ViewModels.Database {
onUpdateTabsButtons: this.container.onUpdateTabsButtons
});
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateNewTab(settingsTab);
},
(error: any) => {
@@ -105,16 +108,21 @@ export default class Database implements ViewModels.Database {
}
);
} else {
this.container.tabsManager.activateTab(settingsTab);
pendingNotificationsPromise.then(
(pendingNotification: DataModels.Notification) => {
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab);
},
(error: any) => {
settingsTab.pendingNotification(undefined);
this.container.tabsManager.activateTab(settingsTab);
}
);
}
};
public readSettings(): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>();
if (this.container.isServerlessEnabled()) {
deferred.resolve();
}
this.container.isRefreshingExplorer(true);
const databaseDataModel: DataModels.Database = <DataModels.Database>{
id: this.id(),
@@ -126,7 +134,9 @@ export default class Database implements ViewModels.Database {
defaultExperience: this.container.defaultExperience()
});
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers();
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
isServerless: this.container.isServerlessEnabled()
});
Q.all([offerInfoPromise]).then(
() => {
this.container.isRefreshingExplorer(false);
@@ -135,6 +145,11 @@ export default class Database implements ViewModels.Database {
offerInfoPromise.valueOf(),
databaseDataModel
);
if (!databaseOffer) {
return;
}
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
const offerThroughputInfo: DataModels.OfferThroughputInfo = {
minimumRUForCollection:
@@ -281,6 +296,49 @@ export default class Database implements ViewModels.Database {
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
}
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
if (!this.container) {
return Q.resolve(undefined);
}
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
this.container.notificationsClient.fetchNotifications().then(
(notifications: DataModels.Notification[]) => {
if (!notifications || notifications.length === 0) {
deferred.resolve(undefined);
return;
}
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
return (
notification.kind === "message" &&
!notification.collectionName &&
notification.databaseName === this.id() &&
notification.description &&
throughputUpdateRegExp.test(notification.description)
);
});
deferred.resolve(pendingNotification);
},
(error: any) => {
Logger.logError(
JSON.stringify({
error: JSON.stringify(error),
accountName: this.container && this.container.databaseAccount(),
databaseName: this.id(),
collectionName: this.id()
}),
"Settings tree node"
);
deferred.resolve(undefined);
}
);
return deferred.promise;
}
private getDeltaCollections(
updatedCollectionsList: DataModels.Collection[]
): { toAdd: DataModels.Collection[]; toDelete: Collection[] } {

View File

@@ -546,43 +546,52 @@ export class ResourceTreeAdapter implements ReactAdapter {
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: createFileContextMenu
? [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item)
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
this.container.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined
);
}
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => this.copyNotebook(item)
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => this.container.downloadFile(item)
}
]
: undefined,
contextMenu: createFileContextMenu && this.createFileContextMenu(item),
data: item
};
}
private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
let items: TreeNodeMenuItem[] = [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item)
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
this.container.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined
);
}
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => this.copyNotebook(item)
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => this.container.downloadFile(item)
}
];
// "Copy to ..." isn't needed if github locations are not available
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter(item => item.label !== "Copy to ...");
}
return items;
}
private copyNotebook = async (item: NotebookContentItem) => {
const content = await this.container.readFile(item);
if (content) {

View File

@@ -2,9 +2,14 @@ import { AccountKind, TagNames, DefaultAccountExperience } from "../../Common/Co
import Explorer from "../../Explorer/Explorer";
import { NotificationsClient } from "./NotificationsClient";
export default class EmulatorExplorerFactory {
public static createExplorer(): Explorer {
const explorer: Explorer = new Explorer();
const explorer: Explorer = new Explorer({
notificationsClient: new NotificationsClient(),
isEmulator: true
});
explorer.databaseAccount({
name: "",
id: "",

View File

@@ -1,24 +1,6 @@
import EmulatorExplorerFactory from "./ExplorerFactory";
import Explorer from "../../Explorer/Explorer";
import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants";
export function initializeExplorer(): Explorer {
const explorer: Explorer = new Explorer();
explorer.databaseAccount({
name: "",
id: "",
location: "",
type: "",
kind: AccountKind.DocumentDB,
tags: {
[TagNames.defaultExperience]: DefaultAccountExperience.DocumentDB
},
properties: {
documentEndpoint: "",
tableEndpoint: "",
gremlinEndpoint: "",
cassandraEndpoint: ""
}
});
explorer.isAccountReady(true);
return explorer;
return EmulatorExplorerFactory.createExplorer();
}

View File

@@ -0,0 +1,16 @@
import Q from "q";
import * as DataModels from "../../Contracts/DataModels";
import { NotificationsClientBase } from "../../Common/NotificationsClientBase";
export class NotificationsClient extends NotificationsClientBase {
private static readonly _notificationsApiSuffix: string = "/api/notifications";
public constructor() {
super(NotificationsClient._notificationsApiSuffix);
}
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
// no notifications for the emulator
return Q([]);
}
}

View File

@@ -1,8 +1,14 @@
import Explorer from "../../Explorer/Explorer";
import { NotificationsClient } from "./NotificationsClient";
export default class HostedExplorerFactory {
public createExplorer(): Explorer {
return new Explorer();
const explorer = new Explorer({
notificationsClient: new NotificationsClient(),
isEmulator: false
});
return explorer;
}
public static reInitializeDocumentClientUtilityForExplorer(explorer: Explorer): void {

View File

@@ -0,0 +1,9 @@
import { NotificationsClientBase } from "../../Common/NotificationsClientBase";
export class NotificationsClient extends NotificationsClientBase {
private static readonly _notificationsApiSuffix: string = "/api/guest/notifications";
public constructor() {
super(NotificationsClient._notificationsApiSuffix);
}
}

View File

@@ -0,0 +1,13 @@
import Explorer from "../../Explorer/Explorer";
import { NotificationsClient } from "./NotificationsClient";
export default class PortalExplorerFactory {
public createExplorer(): Explorer {
var explorer = new Explorer({
notificationsClient: new NotificationsClient(),
isEmulator: false
});
return explorer;
}
}

View File

@@ -1,8 +1,10 @@
import PortalExplorerFactory from "./ExplorerFactory";
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
import Explorer from "../../Explorer/Explorer";
export function initializeExplorer(): Explorer {
const explorer = new Explorer();
const portalExplorerFactory = new PortalExplorerFactory();
const explorer = portalExplorerFactory.createExplorer();
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
return explorer;

View File

@@ -0,0 +1,9 @@
import { NotificationsClientBase } from "../../Common/NotificationsClientBase";
export class NotificationsClient extends NotificationsClientBase {
private static readonly _notificationsApiSuffix: string = "/api/notifications";
public constructor() {
super(NotificationsClient._notificationsApiSuffix);
}
}

View File

@@ -9,7 +9,10 @@ describe("TabRouteHandler", () => {
let tabRouteHandler: TabRouteHandler;
beforeAll(() => {
(<any>window).dataExplorer = new Explorer(); // create a mock to avoid null refs
(<any>window).dataExplorer = new Explorer({
notificationsClient: null,
isEmulator: false
}); // create a mock to avoid null refs
});
beforeEach(() => {

View File

@@ -8,6 +8,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { userContext } from "../UserContext";
import { createUpdateCassandraKeyspace } from "../Utils/arm/generatedClients/2020-04-01/cassandraResources";
export class AddDbUtilities {
// todo - remove any
@@ -47,7 +48,6 @@ export class AddDbUtilities {
// todo - remove any
public static async createCassandraKeyspace(
armEndpoint: string,
params: DataModels.RpParameters,
rpOptions: DataModels.RpOptions
): Promise<any> {
@@ -70,9 +70,11 @@ export class AddDbUtilities {
}
try {
await AddDbUtilities.getRpClient<DataModels.CreateDatabaseWithRpResponse>(armEndpoint).putAsync(
AddDbUtilities._getCassandraKeyspaceUri(params),
DataExplorerConstants.ArmApiVersions.publicVersion,
await createUpdateCassandraKeyspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount?.name,
params.db,
rpPayloadToCreateKeyspace
);
} catch (reason) {
@@ -159,10 +161,7 @@ export class AddDbUtilities {
}
private static _handleCreationError(reason: any, params: DataModels.RpParameters, dbType: string = "database") {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error creating ${dbType}: ${JSON.stringify(reason)}, Payload: ${params}`
);
NotificationConsoleUtils.logConsoleError(`Error creating ${dbType}: ${JSON.stringify(reason)}, Payload: ${params}`);
if (reason.status === HttpStatusCodes.Forbidden) {
sendMessage({ type: MessageTypes.ForbiddenError });
return;

View File

@@ -114,13 +114,17 @@ export default class TelemetryProcessor {
return validTimestamp;
}
private static getData(data?: any): any {
private static getData(data: any = {}): any {
if (typeof data === "string") {
data = { message: data };
}
return {
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
authType: (window as any).authType,
subscriptionId: userContext.subscriptionId,
platform: configContext.platform,
...(data ? data : [])
env: process.env.NODE_ENV,
...data
};
}
}

View File

@@ -11,6 +11,7 @@ interface UserContext {
authorizationToken?: string;
resourceToken?: string;
defaultExperience?: DefaultAccountExperienceType;
useSDKOperations?: boolean;
}
const userContext: Readonly<UserContext> = {} as const;

View File

@@ -60,7 +60,7 @@ describe("AuthorizationUtils", () => {
});
describe("displayTokenRenewalPromptForStatus()", () => {
let explorer = new Explorer() as jest.Mocked<Explorer>;
let explorer = new Explorer({} as any) as jest.Mocked<Explorer>;
beforeEach(() => {
jest.clearAllMocks();

View File

@@ -39,7 +39,7 @@ export async function createUpdateCassandraKeyspace(
body: Types.CassandraKeyspaceCreateUpdateParameters
): Promise<Types.CassandraKeyspaceGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB Cassandra keyspace. */
@@ -73,7 +73,7 @@ export async function updateCassandraKeyspaceThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB Cassandra Keyspace from manual throughput to autoscale */
@@ -131,7 +131,7 @@ export async function createUpdateCassandraTable(
body: Types.CassandraTableCreateUpdateParameters
): Promise<Types.CassandraTableGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}/tables/${tableName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB Cassandra table. */
@@ -168,7 +168,7 @@ export async function updateCassandraTableThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/cassandraKeyspaces/${keyspaceName}/tables/${tableName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB Cassandra table from manual throughput to autoscale */

View File

@@ -27,13 +27,7 @@ export async function update(
body: Types.DatabaseAccountUpdateParameters
): Promise<Types.DatabaseAccountGetResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
return armRequest({
host: configContext.ARM_ENDPOINT,
path,
method: "PATCH",
apiVersion,
body: JSON.stringify(body)
});
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body });
}
/* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */
@@ -44,7 +38,7 @@ export async function createOrUpdate(
body: Types.DatabaseAccountCreateUpdateParameters
): Promise<Types.DatabaseAccountGetResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB database account. */
@@ -61,7 +55,7 @@ export async function failoverPriorityChange(
body: Types.FailoverPolicies
): Promise<void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/failoverPriorityChange`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body });
}
/* Lists all the Azure Cosmos DB database accounts available under the subscription. */
@@ -107,7 +101,7 @@ export async function offlineRegion(
body: Types.RegionForOnlineOffline
): Promise<void | Types.ErrorResponse> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/offlineRegion`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body });
}
/* Online the specified region for the specified Azure Cosmos DB database account. */
@@ -118,7 +112,7 @@ export async function onlineRegion(
body: Types.RegionForOnlineOffline
): Promise<void | Types.ErrorResponse> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/onlineRegion`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body });
}
/* Lists the read-only access keys for the specified Azure Cosmos DB database account. */
@@ -149,7 +143,7 @@ export async function regenerateKey(
body: Types.DatabaseAccountRegenerateKeyParameters
): Promise<void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/regenerateKey`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body });
}
/* Checks that the Azure Cosmos DB account name already exists. A valid account name may contain only lowercase letters, numbers, and the '-' character, and must be between 3 and 50 characters. */

View File

@@ -39,7 +39,7 @@ export async function createUpdateGremlinDatabase(
body: Types.GremlinDatabaseCreateUpdateParameters
): Promise<Types.GremlinDatabaseGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB Gremlin database. */
@@ -73,7 +73,7 @@ export async function updateGremlinDatabaseThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB Gremlin database from manual throughput to autoscale */
@@ -131,7 +131,7 @@ export async function createUpdateGremlinGraph(
body: Types.GremlinGraphCreateUpdateParameters
): Promise<Types.GremlinGraphGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/graphs/${graphName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB Gremlin graph. */
@@ -168,7 +168,7 @@ export async function updateGremlinGraphThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/graphs/${graphName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB Gremlin graph from manual throughput to autoscale */

View File

@@ -39,7 +39,7 @@ export async function createUpdateMongoDBDatabase(
body: Types.MongoDBDatabaseCreateUpdateParameters
): Promise<Types.MongoDBDatabaseGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB MongoDB database. */
@@ -73,7 +73,7 @@ export async function updateMongoDBDatabaseThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB MongoDB database from manual throughput to autoscale */
@@ -131,7 +131,7 @@ export async function createUpdateMongoDBCollection(
body: Types.MongoDBCollectionCreateUpdateParameters
): Promise<Types.MongoDBCollectionGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/collections/${collectionName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB MongoDB Collection. */
@@ -168,7 +168,7 @@ export async function updateMongoDBCollectionThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/collections/${collectionName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB MongoDB collection from manual throughput to autoscale */

View File

@@ -39,7 +39,7 @@ export async function createUpdateSqlDatabase(
body: Types.SqlDatabaseCreateUpdateParameters
): Promise<Types.SqlDatabaseGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB SQL database. */
@@ -73,7 +73,7 @@ export async function updateSqlDatabaseThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB SQL database from manual throughput to autoscale */
@@ -131,7 +131,7 @@ export async function createUpdateSqlContainer(
body: Types.SqlContainerCreateUpdateParameters
): Promise<Types.SqlContainerGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB SQL container. */
@@ -168,7 +168,7 @@ export async function updateSqlContainerThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB SQL container from manual throughput to autoscale */
@@ -231,7 +231,7 @@ export async function createUpdateSqlStoredProcedure(
body: Types.SqlStoredProcedureCreateUpdateParameters
): Promise<Types.SqlStoredProcedureGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/storedProcedures/${storedProcedureName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB SQL storedProcedure. */
@@ -283,7 +283,7 @@ export async function createUpdateSqlUserDefinedFunction(
body: Types.SqlUserDefinedFunctionCreateUpdateParameters
): Promise<Types.SqlUserDefinedFunctionGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/userDefinedFunctions/${userDefinedFunctionName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB SQL userDefinedFunction. */
@@ -335,7 +335,7 @@ export async function createUpdateSqlTrigger(
body: Types.SqlTriggerCreateUpdateParameters
): Promise<Types.SqlTriggerGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/triggers/${triggerName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB SQL trigger. */

View File

@@ -39,7 +39,7 @@ export async function createUpdateTable(
body: Types.TableCreateUpdateParameters
): Promise<Types.TableGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/tables/${tableName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Deletes an existing Azure Cosmos DB Table. */
@@ -73,7 +73,7 @@ export async function updateTableThroughput(
body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/tables/${tableName}/throughputSettings/default`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
}
/* Migrate an Azure Cosmos DB Table from manual throughput to autoscale */

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,9 @@ Instead, generate ARM clients that consume this function with stricter typing.
*/
import promiseRetry, { AbortError } from "p-retry";
import { ErrorResponse } from "./generatedClients/2020-04-01/types";
import { userContext } from "../../UserContext";
interface ErrorResponse {
error: {
code: string;
message: string;
};
}
interface ARMError extends Error {
code: string;
}
@@ -40,8 +34,8 @@ export async function armRequest<T>({ host, path, apiVersion, method, body: requ
});
if (!response.ok) {
const errorResponse = (await response.json()) as ErrorResponse;
const error = new Error(errorResponse.error?.message) as ARMError;
error.code = errorResponse.error.code;
const error = new Error(errorResponse.message) as ARMError;
error.code = errorResponse.code;
throw error;
}
@@ -84,8 +78,8 @@ async function getOperationStatus(operationStatusUrl: string) {
});
if (!response.ok) {
const errorResponse = (await response.json()) as ErrorResponse;
const error = new Error(errorResponse.error?.message) as ARMError;
error.code = errorResponse.error.code;
const error = new Error(errorResponse.message) as ARMError;
error.code = errorResponse.code;
throw new AbortError(error);
}
const body = (await response.json()) as OperationResponse;

View File

@@ -1,81 +1,79 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"strictNullChecks": true,
"strict": true,
"noUnusedLocals": true
},
"files": [
"./src/AuthType.ts",
"./src/Bindings/BindingHandlersRegisterer.ts",
"./src/Bindings/ReactBindingHandler.ts",
"./src/Common/ArrayHashMap.ts",
"./src/Common/Constants.ts",
"./src/Common/DeleteFeedback.ts",
"./src/Common/HashMap.ts",
"./src/Common/HeadersUtility.ts",
"./src/Common/Logger.ts",
"./src/Common/MessageHandler.ts",
"./src/Common/MongoUtility.ts",
"./src/Common/ObjectCache.ts",
"./src/Common/ThemeUtility.ts",
"./src/Common/UrlUtility.ts",
"./src/Common/dataAccess/sendNotificationForError.ts",
"./src/ConfigContext.ts",
"./src/Contracts/ActionContracts.ts",
"./src/Contracts/DataModels.ts",
"./src/Contracts/Diagnostics.ts",
"./src/Contracts/ExplorerContracts.ts",
"./src/Contracts/Versions.ts",
"./src/Controls/Heatmap/Heatmap.ts",
"./src/Controls/Heatmap/HeatmapDatatypes.ts",
"./src/DefaultAccountExperienceType.ts",
"./src/Definitions/globals.d.ts",
"./src/Definitions/html.d.ts",
"./src/Definitions/jquery-ui.d.ts",
"./src/Definitions/jquery.d.ts",
"./src/Definitions/plotly.js-cartesian-dist.d-min.ts",
"./src/Definitions/svg.d.ts",
"./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts",
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
"./src/Explorer/Controls/SmartUi/InputUtils.ts",
"./src/Explorer/Notebook/FileSystemUtil.ts",
"./src/Explorer/Notebook/NTeractUtil.ts",
"./src/Explorer/Notebook/NotebookComponent/actions.ts",
"./src/Explorer/Notebook/NotebookComponent/loadTransform.ts",
"./src/Explorer/Notebook/NotebookComponent/reducers.ts",
"./src/Explorer/Notebook/NotebookComponent/types.ts",
"./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/Notebook/NotebookUtil.ts",
"./src/Explorer/Panes/PaneComponents.ts",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts",
"./src/Explorer/Tables/Constants.ts",
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
"./src/Explorer/Tabs/TabComponents.ts",
"./src/GitHub/GitHubConnector.ts",
"./src/Index.ts",
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
"./src/PlatformType.ts",
"./src/ReactDevTools.ts",
"./src/ResourceProvider/IResourceProviderClient.ts",
"./src/Shared/ExplorerSettings.ts",
"./src/Shared/StorageUtility.ts",
"./src/Shared/StringUtility.ts",
"./src/Shared/Telemetry/TelemetryConstants.ts",
"./src/Shared/Telemetry/TelemetryProcessor.ts",
"./src/Shared/appInsights.ts",
"./src/UserContext.ts",
"./src/Utils/Base64Utils.ts",
"./src/Utils/GitHubUtils.ts",
"./src/Utils/MessageValidation.ts",
"./src/Utils/OfferUtils.ts",
"./src/Utils/StringUtils.ts",
"./src/Utils/arm/generatedClients/2020-04-01/types.ts",
"./src/quickstart.ts",
"./src/setupTests.ts",
"./src/workers/upload/definitions.ts"
],
"include": []
}
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"strictNullChecks": true,
"strict": true,
"noUnusedLocals": true
},
"files": [
"./src/AuthType.ts",
"./src/Bindings/BindingHandlersRegisterer.ts",
"./src/Bindings/ReactBindingHandler.ts",
"./src/Common/ArrayHashMap.ts",
"./src/Common/Constants.ts",
"./src/Common/DeleteFeedback.ts",
"./src/Common/HashMap.ts",
"./src/Common/HeadersUtility.ts",
"./src/Common/Logger.ts",
"./src/Common/MessageHandler.ts",
"./src/Common/MongoUtility.ts",
"./src/Common/ObjectCache.ts",
"./src/Common/ThemeUtility.ts",
"./src/Common/UrlUtility.ts",
"./src/Common/dataAccess/sendNotificationForError.ts",
"./src/ConfigContext.ts",
"./src/Contracts/ActionContracts.ts",
"./src/Contracts/DataModels.ts",
"./src/Contracts/Diagnostics.ts",
"./src/Contracts/ExplorerContracts.ts",
"./src/Contracts/Versions.ts",
"./src/Controls/Heatmap/Heatmap.ts",
"./src/Controls/Heatmap/HeatmapDatatypes.ts",
"./src/Definitions/globals.d.ts",
"./src/Definitions/html.d.ts",
"./src/Definitions/jquery-ui.d.ts",
"./src/Definitions/jquery.d.ts",
"./src/Definitions/plotly.js-cartesian-dist.d-min.ts",
"./src/Definitions/svg.d.ts",
"./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts",
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
"./src/Explorer/Controls/SmartUi/InputUtils.ts",
"./src/Explorer/Notebook/FileSystemUtil.ts",
"./src/Explorer/Notebook/NTeractUtil.ts",
"./src/Explorer/Notebook/NotebookComponent/actions.ts",
"./src/Explorer/Notebook/NotebookComponent/loadTransform.ts",
"./src/Explorer/Notebook/NotebookComponent/reducers.ts",
"./src/Explorer/Notebook/NotebookComponent/types.ts",
"./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/Notebook/NotebookUtil.ts",
"./src/Explorer/Panes/PaneComponents.ts",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts",
"./src/Explorer/Tables/Constants.ts",
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
"./src/Explorer/Tabs/TabComponents.ts",
"./src/GitHub/GitHubConnector.ts",
"./src/Index.ts",
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
"./src/PlatformType.ts",
"./src/ReactDevTools.ts",
"./src/ResourceProvider/IResourceProviderClient.ts",
"./src/Shared/ExplorerSettings.ts",
"./src/Shared/StorageUtility.ts",
"./src/Shared/StringUtility.ts",
"./src/Shared/Telemetry/TelemetryConstants.ts",
"./src/Shared/Telemetry/TelemetryProcessor.ts",
"./src/Shared/appInsights.ts",
"./src/UserContext.ts",
"./src/Utils/GitHubUtils.ts",
"./src/Utils/MessageValidation.ts",
"./src/Utils/OfferUtils.ts",
"./src/Utils/StringUtils.ts",
"./src/Utils/arm/generatedClients/2020-04-01/types.ts",
"./src/quickstart.ts",
"./src/setupTests.ts",
"./src/workers/upload/definitions.ts"
],
"include": []
}

View File

@@ -102,31 +102,31 @@ interface Property {
}[];
}
const propertyToType = (property: Property, prop: string) => {
const propertyToType = (property: Property, prop: string, required: boolean) => {
if (property) {
if (property.allOf) {
outputTypes.push(`
/* ${property.description || "undocumented"} */
${property.readOnly ? "readonly " : ""}${prop}: ${property.allOf
.map((allof: { $ref: string }) => refToType(allof.$ref))
.join(" & ")}`);
${property.readOnly ? "readonly " : ""}${prop}${
required ? "" : "?"
}: ${property.allOf.map((allof: { $ref: string }) => refToType(allof.$ref)).join(" & ")}`);
} else if (property.$ref) {
const type = refToType(property.$ref);
outputTypes.push(`
/* ${property.description || "undocumented"} */
${property.readOnly ? "readonly " : ""}${prop}: ${type}
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${type}
`);
} else if (property.type === "array") {
const type = refToType(property.items.$ref);
outputTypes.push(`
/* ${property.description || "undocumented"} */
${property.readOnly ? "readonly " : ""}${prop}: ${type}[]
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${type}[]
`);
} else if (property.type === "object") {
const type = refToType(property.$ref);
outputTypes.push(`
/* ${property.description || "undocumented"} */
${property.readOnly ? "readonly " : ""}${prop}: ${type}
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${type}
`);
} else {
if (property.type === undefined) {
@@ -135,7 +135,7 @@ const propertyToType = (property: Property, prop: string) => {
}
outputTypes.push(`
/* ${property.description || "undocumented"} */
${property.readOnly ? "readonly " : ""}${prop}: ${
${property.readOnly ? "readonly " : ""}${prop}${required ? "" : "?"}: ${
propertyMap[property.type] ? propertyMap[property.type] : property.type
}`);
}
@@ -166,7 +166,7 @@ async function main() {
}
for (const prop in schema.definitions[definition].properties) {
const property = schema.definitions[definition].properties[prop];
propertyToType(property, prop);
propertyToType(property, prop, schema.definitions[definition].required?.includes(prop));
}
outputTypes.push(`}`);
outputTypes.push("\n\n");
@@ -245,7 +245,7 @@ async function main() {
) : Promise<${responseType(operation, "Types")}> {
const path = \`${path.replace(/{/g, "${")}\`
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "${method.toLocaleUpperCase()}", apiVersion, ${
bodyParameter ? "body: JSON.stringify(body)" : ""
bodyParameter ? "body" : ""
} })
}
`);