Compare commits

..

1 Commits

Author SHA1 Message Date
Steve Faulkner
f738723f5a Remove ExplorerOptions 2020-08-12 21:43:58 -05:00
79 changed files with 1393 additions and 2223 deletions

View File

@@ -41,7 +41,6 @@ module.exports = {
"@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-extraneous-class": "error",
"no-null/no-null": "error", "no-null/no-null": "error",
"@typescript-eslint/no-explicit-any": "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"
} }
}; };

View File

@@ -7,7 +7,7 @@
"test": "cypress run", "test": "cypress run",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/", "wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"", "test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless", "test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
"test:debug": "cypress open" "test:debug": "cypress open"
}, },
"devDependencies": { "devDependencies": {

24
package-lock.json generated
View File

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

View File

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

View File

@@ -3,8 +3,8 @@
"offerThroughput": 400, "offerThroughput": 400,
"databaseLevelThroughput": false, "databaseLevelThroughput": false,
"collectionId": "Persons", "collectionId": "Persons",
"createNewDatabase": true, "rupmEnabled": false,
"partitionKey": { "kind": "Hash", "paths": ["/firstname"], "version": 1 }, "partitionKey": { "kind": "Hash", "paths": ["/firstname"] },
"data": [ "data": [
{ {
"firstname": "Eva", "firstname": "Eva",

View File

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

View File

@@ -6,14 +6,19 @@ import * as ViewModels from "../Contracts/ViewModels";
import Q from "q"; import Q from "q";
import { import {
ConflictDefinition, ConflictDefinition,
ContainerDefinition,
ContainerResponse,
DatabaseResponse,
FeedOptions, FeedOptions,
ItemDefinition, ItemDefinition,
PartitionKeyDefinition,
QueryIterator, QueryIterator,
Resource, Resource,
TriggerDefinition, TriggerDefinition
OfferDefinition
} from "@azure/cosmos"; } from "@azure/cosmos";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
import { client } from "./CosmosClient"; import { client } from "./CosmosClient";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { sendCachedDataMessage } from "./MessageHandler"; import { sendCachedDataMessage } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
@@ -197,6 +202,23 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
return [partitionKeyValue]; 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( export function updateDocument(
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
documentId: DocumentId, documentId: DocumentId,
@@ -222,8 +244,7 @@ export function updateOffer(
return Q( return Q(
client() client()
.offer(offer.id) .offer(offer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660) .replace(newOffer, options)
.replace((newOffer as unknown) as OfferDefinition, options)
.then(response => { .then(response => {
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource); return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
}) })
@@ -433,10 +454,6 @@ export function readCollectionQuotaInfo(
} }
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> { export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
if (options.isServerless) {
return Q([]); // Reading offers is not supported for serverless accounts
}
try { try {
if (configContext.platform === Platform.Portal) { if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [ return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
@@ -452,13 +469,6 @@ export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
.offers.readAll() .offers.readAll()
.fetchAll() .fetchAll()
.then(response => response.resources) .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;
})
); );
} }
@@ -477,6 +487,89 @@ export function readOffer(requestedResource: DataModels.Offer, options: any): Q.
); );
} }
export function getOrCreateDatabaseAndCollection(
request: DataModels.CreateDatabaseAndCollectionRequest,
options: any
): Q.Promise<DataModels.Collection> {
const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput");
const {
databaseId,
databaseLevelThroughput,
collectionId,
partitionKey,
indexingPolicy,
uniqueKeyPolicy,
offerThroughput,
analyticalStorageTtl,
hasAutoPilotV2FeatureFlag
} = request;
const createBody: DatabaseRequest = {
id: databaseId
};
// TODO: replace when SDK support autopilot
const initialHeaders = request.autoPilot
? !hasAutoPilotV2FeatureFlag
? {
[Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({
maxThroughput: request.autoPilot.maxThroughput
})
}
: {
[Constants.HttpHeaders.autoPilotTier]: request.autoPilot.autopilotTier
}
: undefined;
if (databaseLevelThroughput) {
if (request.autoPilot) {
databaseOptions.initialHeaders = initialHeaders;
}
createBody.throughput = offerThroughput;
}
return Q(
client()
.databases.createIfNotExists(createBody, databaseOptions)
.then(response => {
return response.database.containers.create(
{
id: collectionId,
partitionKey: (partitionKey || undefined) as PartitionKeyDefinition,
indexingPolicy: indexingPolicy ? indexingPolicy : undefined,
uniqueKeyPolicy: uniqueKeyPolicy ? uniqueKeyPolicy : undefined,
analyticalStorageTtl: analyticalStorageTtl,
throughput: databaseLevelThroughput || request.autoPilot ? undefined : offerThroughput
} as ContainerRequest, // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
{
initialHeaders: databaseLevelThroughput ? undefined : initialHeaders
}
);
})
.then(containerResponse => containerResponse.resource as DataModels.Collection)
.finally(() => refreshCachedResources(options))
);
}
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> { export function refreshCachedOffers(): Q.Promise<void> {
if (configContext.platform === Platform.Portal) { if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshOffers, []); return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
@@ -505,3 +598,33 @@ export function queryConflicts(
.conflicts.query(query, options); .conflicts.query(query, options);
return Q(documentsIterator); 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

@@ -1,5 +1,6 @@
import * as Constants from "./Constants"; import * as Constants from "./Constants";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ErrorParserUtility from "./ErrorParserUtility";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Q from "q"; import Q from "q";
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
@@ -265,6 +266,42 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
return deferred.promise; 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( export function updateDocument(
collection: ViewModels.CollectionBase, collection: ViewModels.CollectionBase,
documentId: DocumentId, documentId: DocumentId,
@@ -855,3 +892,70 @@ export function readOffer(
return deferred.promise; return deferred.promise;
} }
export function getOrCreateDatabaseAndCollection(
request: DataModels.CreateDatabaseAndCollectionRequest,
options: any = {}
): Q.Promise<DataModels.Collection> {
const deferred: Q.Deferred<DataModels.Collection> = Q.defer<DataModels.Collection>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating a new container ${request.collectionId} for database ${request.databaseId}`
);
DataAccessUtilityBase.getOrCreateDatabaseAndCollection(request, options)
.then(
(collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created container ${request.collectionId}`
);
deferred.resolve(collection);
},
(error: any) => {
const sanitizedError = ErrorParserUtility.replaceKnownError(JSON.stringify(error));
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while creating container ${request.collectionId}:\n ${sanitizedError}`
);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
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

@@ -1,6 +1,7 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import queryString from "querystring"; import queryString from "querystring";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import * as DataExplorerConstants from "../Common/Constants"; import * as DataExplorerConstants from "../Common/Constants";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
@@ -284,35 +285,43 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
} }
export function createMongoCollectionWithProxy( export function createMongoCollectionWithProxy(
params: DataModels.CreateCollectionParams databaseId: string,
collectionId: string,
offerThroughput: number,
shardKey: string,
createDatabase: boolean,
sharedThroughput: boolean,
isSharded: boolean,
autopilotOptions?: DataModels.RpOptions
): Promise<DataModels.Collection> { ): Promise<DataModels.Collection> {
const databaseAccount = userContext.databaseAccount; const databaseAccount = userContext.databaseAccount;
const shardKey: string = params.partitionKey?.paths[0]; const params: DataModels.MongoParameters = {
const mongoParams: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint, resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: params.databaseId, db: databaseId,
coll: params.collectionId, coll: collectionId,
pk: shardKey, pk: shardKey,
offerThroughput: params.offerThroughput, offerThroughput,
cd: params.createNewDatabase, cd: createDatabase,
st: params.databaseLevelThroughput, st: sharedThroughput,
is: !!shardKey, is: isSharded,
rid: "", rid: "",
rtype: "colls", rtype: "colls",
sid: userContext.subscriptionId, sid: userContext.subscriptionId,
rg: userContext.resourceGroup, rg: userContext.resourceGroup,
dba: databaseAccount.name, dba: databaseAccount.name,
isAutoPilot: !!params.autoPilotMaxThroughput, isAutoPilot: false
autoPilotThroughput: params.autoPilotMaxThroughput?.toString()
}; };
if (autopilotOptions) {
params.isAutoPilot = true;
params.autoPilotTier = autopilotOptions[Constants.HttpHeaders.autoPilotTier] as string;
}
const endpoint = getEndpoint(databaseAccount); const endpoint = getEndpoint(databaseAccount);
return window return window
.fetch( .fetch(
`${endpoint}/createCollection?${queryString.stringify( `${endpoint}/createCollection?${queryString.stringify((params as unknown) as queryString.ParsedUrlQueryInput)}`,
(mongoParams as unknown) as queryString.ParsedUrlQueryInput
)}`,
{ {
method: "POST", method: "POST",
headers: { headers: {
@@ -326,7 +335,7 @@ export function createMongoCollectionWithProxy(
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
return errorHandling(response, "creating collection", mongoParams); return errorHandling(response, "creating collection", params);
}); });
} }

View File

@@ -1,46 +0,0 @@
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

@@ -10,8 +10,13 @@ import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils"; import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; import {
import { createCollection } from "./dataAccess/createCollection"; createDocument,
deleteDocument,
getOrCreateDatabaseAndCollection,
queryDocuments,
queryDocumentsPage
} from "./DocumentClientUtilityBase";
import * as ErrorParserUtility from "./ErrorParserUtility"; import * as ErrorParserUtility from "./ErrorParserUtility";
import * as Logger from "./Logger"; import * as Logger from "./Logger";
@@ -36,13 +41,12 @@ export class QueriesClient {
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
"Setting up account for saving queries" "Setting up account for saving queries"
); );
return createCollection({ return getOrCreateDatabaseAndCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
createNewDatabase: true,
databaseId: SavedQueries.DatabaseName, databaseId: SavedQueries.DatabaseName,
partitionKey: QueriesClient.PartitionKey, partitionKey: QueriesClient.PartitionKey,
offerThroughput: SavedQueries.OfferThroughput, offerThroughput: SavedQueries.OfferThroughput,
databaseLevelThroughput: false databaseLevelThroughput: undefined
}) })
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {

View File

@@ -1,81 +0,0 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
jest.mock("../DataAccessUtilityBase");
import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
import { createCollection, constructRpOptions } from "./createCollection";
import { updateUserContext } from "../../UserContext";
describe("createCollection", () => {
const createCollectionParams: CreateCollectionParams = {
createNewDatabase: false,
collectionId: "testContainer",
databaseId: "testDatabase",
databaseLevelThroughput: true,
offerThroughput: 400
};
beforeAll(() => {
updateUserContext({
databaseAccount: {
name: "test"
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
});
it("should call ARM if logged in with AAD", async () => {
window.authType = AuthType.AAD;
await createCollection(createCollectionParams);
expect(armRequest).toHaveBeenCalled();
});
it("should call SDK if not logged in with non-AAD method", async () => {
window.authType = AuthType.MasterKey;
(client as jest.Mock).mockReturnValue({
databases: {
createIfNotExists: () => {
return {
database: {
containers: {
create: () => ({})
}
}
};
}
}
});
await createCollection(createCollectionParams);
expect(client).toHaveBeenCalled();
});
it("constructRpOptions should return the correct options", () => {
expect(constructRpOptions(createCollectionParams)).toEqual({});
const manualThroughputParams: CreateCollectionParams = {
createNewDatabase: false,
collectionId: "testContainer",
databaseId: "testDatabase",
databaseLevelThroughput: false,
offerThroughput: 400
};
expect(constructRpOptions(manualThroughputParams)).toEqual({ throughput: 400 });
const autoPilotThroughputParams: CreateCollectionParams = {
createNewDatabase: false,
collectionId: "testContainer",
databaseId: "testDatabase",
databaseLevelThroughput: false,
offerThroughput: 400,
autoPilotMaxThroughput: 4000
};
expect(constructRpOptions(autoPilotThroughputParams)).toEqual({
autoscaleSettings: {
maxThroughput: 4000
}
});
});
});

View File

@@ -1,371 +0,0 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ErrorParserUtility from "../ErrorParserUtility";
import { AuthType } from "../../AuthType";
import { ContainerResponse, DatabaseResponse } from "@azure/cosmos";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import * as ARMTypes from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
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 { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
import { createDatabase } from "./createDatabase";
export const createCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
let collection: DataModels.Collection;
const clearMessage = logConsoleProgress(
`Creating a new container ${params.collectionId} for database ${params.databaseId}`
);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
if (params.createNewDatabase) {
const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
databaseId: params.databaseId,
databaseLevelThroughput: params.databaseLevelThroughput,
offerThroughput: params.offerThroughput
};
await createDatabase(createDatabaseParams);
}
collection = await createCollectionWithARM(params);
} else if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
collection = await createMongoCollectionWithProxy(params);
} else {
collection = await createCollectionWithSDK(params);
}
} catch (error) {
const sanitizedError = ErrorParserUtility.replaceKnownError(JSON.stringify(error));
logConsoleError(`Error while creating container ${params.collectionId}:\n ${sanitizedError}`);
logError(JSON.stringify(error), "CreateCollection", error.code);
sendNotificationForError(error);
clearMessage();
throw error;
}
logConsoleInfo(`Successfully created container ${params.collectionId}`);
await refreshCachedResources();
clearMessage();
return collection;
};
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
return createSqlContainer(params);
case DefaultAccountExperienceType.MongoDB:
return createMongoCollection(params);
case DefaultAccountExperienceType.Cassandra:
return createCassandraTable(params);
case DefaultAccountExperienceType.Graph:
return createGraph(params);
case DefaultAccountExperienceType.Table:
return createTable(params);
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
};
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.SqlContainerResource = {
id: params.collectionId
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateSqlContainer(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getMongoDBCollection(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.MongoDBCollectionResource = {
id: params.collectionId
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
if (params.partitionKey) {
const partitionKeyPath: string = params.partitionKey.paths[0];
resource.shardKey = { [partitionKeyPath]: "Hash" };
}
const rpPayload: ARMTypes.MongoDBCollectionCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateMongoDBCollection(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getCassandraTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.CassandraTableResource = {
id: params.collectionId
};
if (params.analyticalStorageTtl) {
resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
const rpPayload: ARMTypes.CassandraTableCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateCassandraTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getGremlinGraph(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.GremlinGraphResource = {
id: params.collectionId
};
if (params.indexingPolicy) {
resource.indexingPolicy = params.indexingPolicy;
}
if (params.partitionKey) {
resource.partitionKey = params.partitionKey;
}
if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
}
const rpPayload: ARMTypes.GremlinGraphCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateGremlinGraph(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.databaseId,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
try {
const getResponse = await getTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.collectionId
);
if (getResponse?.properties?.resource) {
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
const resource: ARMTypes.TableResource = {
id: params.collectionId
};
const rpPayload: ARMTypes.TableCreateUpdateParameters = {
properties: {
resource,
options
}
};
const createResponse = await createUpdateTable(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
params.collectionId,
rpPayload
);
return createResponse && (createResponse.properties.resource as DataModels.Collection);
};
export const constructRpOptions = (params: DataModels.CreateDatabaseParams): ARMTypes.CreateUpdateOptions => {
if (params.databaseLevelThroughput) {
return {};
}
if (params.autoPilotMaxThroughput) {
return {
autoscaleSettings: {
maxThroughput: params.autoPilotMaxThroughput
}
};
}
return {
throughput: params.offerThroughput
};
};
const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
const createCollectionBody: ContainerRequest = {
id: params.collectionId,
partitionKey: params.partitionKey || undefined,
indexingPolicy: params.indexingPolicy || undefined,
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
analyticalStorageTtl: params.analyticalStorageTtl
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
const collectionOptions: RequestOptions = {};
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
if (params.databaseLevelThroughput) {
if (params.autoPilotMaxThroughput) {
createDatabaseBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createDatabaseBody.throughput = params.offerThroughput;
}
} else {
if (params.autoPilotMaxThroughput) {
createCollectionBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createCollectionBody.throughput = params.offerThroughput;
}
}
const databaseResponse: DatabaseResponse = await client().databases.createIfNotExists(createDatabaseBody);
const collectionResponse: ContainerResponse = await databaseResponse?.database.containers.create(
createCollectionBody,
collectionOptions
);
return collectionResponse?.resource as DataModels.Collection;
};

View File

@@ -1,251 +0,0 @@
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 {
CassandraKeyspaceCreateUpdateParameters,
GremlinDatabaseCreateUpdateParameters,
MongoDBDatabaseCreateUpdateParameters,
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?.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?.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: MongoDBDatabaseCreateUpdateParameters = {
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: CassandraKeyspaceCreateUpdateParameters = {
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?.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: GremlinDatabaseCreateUpdateParameters = {
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 };
if (params.databaseLevelThroughput) {
if (params.autoPilotMaxThroughput) {
createBody.maxThroughput = params.autoPilotMaxThroughput;
} else {
createBody.throughput = params.offerThroughput;
}
}
const response: DatabaseResponse = await client().databases.create(createBody);
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> { export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`); const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
try { try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) { if (window.authType === AuthType.AAD) {
await deleteCollectionWithARM(databaseId, collectionId); await deleteCollectionWithARM(databaseId, collectionId);
} else { } else {
await client() await client()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
jest.mock("../../Common/DocumentClientUtilityBase"); jest.mock("../../Common/DocumentClientUtilityBase");
jest.mock("../../Common/dataAccess/createCollection");
import * as ko from "knockout"; import * as ko from "knockout";
import * as sinon from "sinon"; import * as sinon from "sinon";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -34,8 +33,8 @@ describe("ContainerSampleGenerator", () => {
databaseId: sampleDatabaseId, databaseId: sampleDatabaseId,
offerThroughput: 400, offerThroughput: 400,
databaseLevelThroughput: false, databaseLevelThroughput: false,
createNewDatabase: true,
collectionId: sampleCollectionId, collectionId: sampleCollectionId,
rupmEnabled: false,
data: [ data: [
{ {
firstname: "Eva", firstname: "Eva",
@@ -100,8 +99,8 @@ describe("ContainerSampleGenerator", () => {
databaseId: sampleDatabaseId, databaseId: sampleDatabaseId,
offerThroughput: 400, offerThroughput: 400,
databaseLevelThroughput: false, databaseLevelThroughput: false,
createNewDatabase: true,
collectionId: sampleCollectionId, collectionId: sampleCollectionId,
rupmEnabled: false,
data: [ data: [
"g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)" "g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)"
] ]

View File

@@ -1,3 +1,4 @@
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import GraphTab from ".././Tabs/GraphTab"; import GraphTab from ".././Tabs/GraphTab";
@@ -5,11 +6,10 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { createDocument } from "../../Common/DocumentClientUtilityBase"; import { createDocument, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateCollectionParams { interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
data: any[]; data: any[];
} }
@@ -54,11 +54,18 @@ export class ContainerSampleGenerator {
} }
private async createContainerAsync(): Promise<ViewModels.Collection> { private async createContainerAsync(): Promise<ViewModels.Collection> {
const createRequest: DataModels.CreateCollectionParams = { const createRequest: DataModels.CreateDatabaseAndCollectionRequest = {
...this.sampleDataFile ...this.sampleDataFile
}; };
await createCollection(createRequest); const options: any = {};
if (this.container.isPreferredApiMongoDB()) {
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[Constants.HttpHeaders.supportSpatialLegacyCoordinates] = true;
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
}
await getOrCreateDatabaseAndCollection(createRequest, options);
await this.container.refreshAllDatabases(); await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId); const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) { if (!database) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import * as Immutable from "immutable"; import * as Immutable from "immutable";
import { ActionsObservable, StateObservable } from "redux-observable"; import { ActionsObservable, StateObservable } from "redux-observable";
import { Subject, empty } from "rxjs"; import { Subject } from "rxjs";
import { toArray } from "rxjs/operators"; import { toArray } from "rxjs/operators";
import { makeNotebookRecord } from "@nteract/commutable"; import { makeNotebookRecord } from "@nteract/commutable";
import { actions, state } from "@nteract/core"; import { actions, state } from "@nteract/core";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { CdbAppState, makeCdbRecord } from "./types"; import { CdbAppState, makeCdbRecord } from "./types";
import { launchWebSocketKernelEpic, autoStartKernelEpic } from "./epics"; import { launchWebSocketKernelEpic } from "./epics";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookUtil } from "../NotebookUtil";
import { sessions } from "rx-jupyter"; import { sessions } from "rx-jupyter";
@@ -74,6 +74,11 @@ describe("Extract kernel from notebook", () => {
}); });
}); });
describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef";
const kernelRef = "fake";
const initialState = { const initialState = {
app: state.makeAppRecord({ app: state.makeAppRecord({
host: state.makeJupyterHostRecord({ host: state.makeJupyterHostRecord({
@@ -110,12 +115,6 @@ const initialState = {
}) })
}; };
describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef";
const kernelRef = "fake";
it("launches remote kernels", async () => { it("launches remote kernels", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState); const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
@@ -491,55 +490,3 @@ 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 { webSocket } from "rxjs/webSocket";
import { ActionsObservable, StateObservable } from "redux-observable"; import { ActionsObservable, StateObservable } from "redux-observable";
import { ofType } 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 it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") { if (!model || model.type !== "notebook") {
return EMPTY; return empty();
} }
const cellOrder = selectors.notebook.cellOrder(model); const cellOrder = selectors.notebook.cellOrder(model);
@@ -90,40 +90,7 @@ 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"
})
);
}) })
); );
}; };
@@ -321,7 +288,7 @@ export const launchWebSocketKernelEpic = (
const state = state$.value; const state = state$.value;
const host = selectors.currentHost(state); const host = selectors.currentHost(state);
if (host.type !== "jupyter") { if (host.type !== "jupyter") {
return EMPTY; return empty();
} }
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host); const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
serverConfig.userPuid = getUserPuid(); serverConfig.userPuid = getUserPuid();
@@ -332,7 +299,7 @@ export const launchWebSocketKernelEpic = (
const content = selectors.content(state, { contentRef }); const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") { if (!content || content.type !== "notebook") {
return EMPTY; return empty();
} }
let kernelSpecToLaunch = kernelSpecName; let kernelSpecToLaunch = kernelSpecName;
@@ -546,26 +513,26 @@ const changeWebSocketKernelEpic = (
const state = state$.value; const state = state$.value;
const host = selectors.currentHost(state); const host = selectors.currentHost(state);
if (host.type !== "jupyter") { if (host.type !== "jupyter") {
return EMPTY; return empty();
} }
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host); const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
if (!oldKernelRef) { if (!oldKernelRef) {
return EMPTY; return empty();
} }
const oldKernel = selectors.kernel(state, { kernelRef: oldKernelRef }); const oldKernel = selectors.kernel(state, { kernelRef: oldKernelRef });
if (!oldKernel || oldKernel.type !== "websocket") { if (!oldKernel || oldKernel.type !== "websocket") {
return EMPTY; return empty();
} }
const { sessionId } = oldKernel; const { sessionId } = oldKernel;
if (!sessionId) { if (!sessionId) {
return EMPTY; return empty();
} }
const content = selectors.content(state, { contentRef }); const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") { if (!content || content.type !== "notebook") {
return EMPTY; return empty();
} }
const { const {
filepath, filepath,
@@ -626,7 +593,7 @@ const focusInitialCodeCellEpic = (
// If it's not a notebook, we shouldn't be here // If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") { if (!model || model.type !== "notebook") {
return EMPTY; return empty();
} }
const cellOrder = selectors.notebook.cellOrder(model); const cellOrder = selectors.notebook.cellOrder(model);
@@ -641,7 +608,7 @@ const focusInitialCodeCellEpic = (
); );
} }
return EMPTY; return empty();
}) })
); );
}; };
@@ -694,7 +661,7 @@ const notificationsToUserEpic = (
break; break;
} }
} }
return EMPTY; return empty();
}) })
); );
}; };
@@ -734,7 +701,7 @@ const handleKernelConnectionLostEpic = (
if (explorer) { if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg); explorer.showOkModalDialog("kernel restarts", msg);
} }
return of(EMPTY); return of(empty());
} }
return concat( return concat(
@@ -847,7 +814,7 @@ const closeUnsupportedMimetypesEpic = (
explorer.showOkModalDialog("File cannot be rendered", msg); explorer.showOkModalDialog("File cannot be rendered", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
} }
return EMPTY; return empty();
}) })
); );
}; };
@@ -875,14 +842,13 @@ const closeContentFailedToFetchEpic = (
explorer.showOkModalDialog("Failure to load", msg); explorer.showOkModalDialog("Failure to load", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
} }
return EMPTY; return empty();
}) })
); );
}; };
export const allEpics = [ export const allEpics = [
addInitialCodeCellEpic, addInitialCodeCellEpic,
autoStartKernelEpic,
focusInitialCodeCellEpic, focusInitialCodeCellEpic,
notificationsToUserEpic, notificationsToUserEpic,
launchWebSocketKernelEpic, launchWebSocketKernelEpic,

View File

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

View File

@@ -9,15 +9,18 @@ import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants"; import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import editable from "../../Common/EditableUtility"; import editable from "../../Common/EditableUtility";
import EnvironmentUtility from "../../Common/EnvironmentUtility";
import Q from "q";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { createMongoCollectionWithARM, createMongoCollectionWithProxy } from "../../Common/MongoProxyClient";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent"; import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { HashMap } from "../../Common/HashMap"; import { HashMap } from "../../Common/HashMap";
import { PlatformType } from "../../PlatformType"; import { PlatformType } from "../../PlatformType";
import { refreshCachedResources } from "../../Common/DocumentClientUtilityBase"; import { refreshCachedResources, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { userContext } from "../../UserContext";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>; isPreferredApiTable: ko.Computed<boolean>;
@@ -326,7 +329,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if ( if (
!this.container.isEmulator && configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() && !this.container.isTryCosmosDBSubscription() &&
this.container.getPlatformType() !== PlatformType.Portal this.container.getPlatformType() !== PlatformType.Portal
) { ) {
@@ -338,7 +341,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
this.costsVisible = ko.pureComputed(() => { this.costsVisible = ko.pureComputed(() => {
return !this.container.isEmulator; return configContext.platform !== Platform.Emulator;
}); });
this.maxCollectionsReached = ko.computed<boolean>(() => { this.maxCollectionsReached = ko.computed<boolean>(() => {
@@ -808,6 +811,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
let databaseId: string = this.databaseCreateNew() ? this.databaseId().trim() : this.databaseId(); let databaseId: string = this.databaseCreateNew() ? this.databaseId().trim() : this.databaseId();
let collectionId: string = this.collectionId().trim(); let collectionId: string = this.collectionId().trim();
let rupm: boolean = this.rupm() === Constants.RUPMStates.on;
let indexingPolicy: DataModels.IndexingPolicy; let indexingPolicy: DataModels.IndexingPolicy;
// todo - remove mongo indexing policy ticket # 616274 // todo - remove mongo indexing policy ticket # 616274
@@ -824,28 +828,130 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
this.formErrors(""); this.formErrors("");
this.isExecuting(true); this.isExecuting(true);
const databaseLevelThroughput: boolean = this.databaseCreateNew() const createRequest: DataModels.CreateDatabaseAndCollectionRequest = {
? this.databaseCreateNewShared()
: this.databaseHasSharedOffer() && !this.collectionWithThroughputInShared();
const autoPilotMaxThroughput: number = databaseLevelThroughput
? this.isSharedAutoPilotSelected() && this.sharedAutoPilotThroughput()
: this.isAutoPilotSelected() && this.autoPilotThroughput();
const createCollectionParams: DataModels.CreateCollectionParams = {
createNewDatabase: this.databaseCreateNew(),
collectionId, collectionId,
databaseId, databaseId,
databaseLevelThroughput,
offerThroughput, offerThroughput,
analyticalStorageTtl: this._getAnalyticalStorageTtl(), databaseLevelThroughput: this.databaseHasSharedOffer() && !this.collectionWithThroughputInShared(),
autoPilotMaxThroughput, rupmEnabled: rupm,
indexingPolicy,
partitionKey, partitionKey,
uniqueKeyPolicy indexingPolicy,
uniqueKeyPolicy,
autoPilot,
analyticalStorageTtl: this._getAnalyticalStorageTtl(),
hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag()
}; };
createCollection(createCollectionParams).then( const options: any = {};
if (this.container.isPreferredApiMongoDB()) {
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[Constants.HttpHeaders.supportSpatialLegacyCoordinates] = true;
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
}
const databaseCreateNew = this.databaseCreateNew();
const useDatabaseSharedOffer = this.shouldUseDatabaseThroughput();
const isSharded: boolean = !!partitionKeyPath;
const autopilotSettings: DataModels.RpOptions = this._getAutopilotSettings();
let createCollectionFunc: () => Q.Promise<DataModels.Collection | DataModels.CreateCollectionWithRpResponse>;
if (this.container.isPreferredApiMongoDB()) {
const isFixedCollectionWithSharedThroughputBeingCreated =
this.container.isFixedCollectionWithSharedThroughputSupported() &&
!this.isUnlimitedStorageSelected() &&
this.databaseHasSharedOffer();
const isAadUser = EnvironmentUtility.isAadUser();
// note: v3 autopilot not supported yet for Mongo fixed collections (only tier supported)
if (!isAadUser || isFixedCollectionWithSharedThroughputBeingCreated) {
createCollectionFunc = () =>
Q(
createMongoCollectionWithProxy(
databaseId,
collectionId,
offerThroughput,
partitionKeyPath,
databaseCreateNew,
useDatabaseSharedOffer,
isSharded,
autopilotSettings
)
);
} else {
createCollectionFunc = () =>
Q(
createMongoCollectionWithARM(
this.container.armEndpoint(),
databaseId,
this._getAnalyticalStorageTtl(),
collectionId,
offerThroughput,
partitionKeyPath,
databaseCreateNew,
useDatabaseSharedOffer,
isSharded,
autopilotSettings
)
);
}
} else if (this.container.isPreferredApiTable() && EnvironmentUtility.isAadUser()) {
createCollectionFunc = () =>
Q(
AddCollectionUtility.Utilities.createAzureTableWithARM(
this.container.armEndpoint(),
createRequest,
autopilotSettings
)
);
} else if (this.container.isPreferredApiGraph() && EnvironmentUtility.isAadUser()) {
createCollectionFunc = () =>
Q(
AddCollectionUtility.CreateCollectionUtilities.createGremlinGraph(
this.container.armEndpoint(),
databaseId,
collectionId,
indexingPolicy,
offerThroughput,
partitionKeyPath,
partitionKey.version,
databaseCreateNew,
useDatabaseSharedOffer,
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
autopilotSettings
)
);
} else if (this.container.isPreferredApiDocumentDB() && EnvironmentUtility.isAadUser()) {
createCollectionFunc = () =>
Q(
AddCollectionUtility.CreateSqlCollectionUtilities.createSqlCollection(
this.container.armEndpoint(),
databaseId,
this._getAnalyticalStorageTtl(),
collectionId,
indexingPolicy,
offerThroughput,
partitionKeyPath,
partitionKey.version,
databaseCreateNew,
useDatabaseSharedOffer,
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
uniqueKeyPolicy,
autopilotSettings
)
);
} else {
createCollectionFunc = () => getOrCreateDatabaseAndCollection(createRequest, options);
}
createCollectionFunc().then(
() => { () => {
this.isExecuting(false); this.isExecuting(false);
this.close(); this.close();
@@ -1128,6 +1234,35 @@ export default class AddCollectionPane extends ContextualPaneBase {
return undefined; return undefined;
} }
private _getAutopilotSettings(): DataModels.RpOptions {
if (
(!this.hasAutoPilotV2FeatureFlag() &&
this.databaseCreateNewShared() &&
this.isSharedAutoPilotSelected() &&
this.sharedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() &&
this.databaseCreateNewShared() &&
this.isSharedAutoPilotSelected() &&
this.selectedSharedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
[Constants.HttpHeaders.autoPilotThroughput]: { maxThroughput: this.sharedAutoPilotThroughput() * 1 }
}
: { [Constants.HttpHeaders.autoPilotTier]: this.selectedSharedAutoPilotTier().toString() };
}
if (
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.autoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
[Constants.HttpHeaders.autoPilotThroughput]: { maxThroughput: this.autoPilotThroughput() * 1 }
}
: { [Constants.HttpHeaders.autoPilotTier]: this.selectedAutoPilotTier().toString() };
}
return undefined;
}
private _calculateNumberOfPartitions(): number { private _calculateNumberOfPartitions(): number {
// Note: this will not validate properly on accounts that have been set up for custom partitioning, // Note: this will not validate properly on accounts that have been set up for custom partitioning,

View File

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

View File

@@ -14,9 +14,10 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility"; import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { PlatformType } from "../../PlatformType"; import { PlatformType } from "../../PlatformType";
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { configContext, Platform } from "../../ConfigContext";
export default class AddDatabasePane extends ContextualPaneBase { export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>; public defaultExperience: ko.Computed<string>;
@@ -179,7 +180,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if ( if (
!this.container.isEmulator && configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() && !this.container.isTryCosmosDBSubscription() &&
this.container.getPlatformType() !== PlatformType.Portal this.container.getPlatformType() !== PlatformType.Portal
) { ) {
@@ -202,7 +203,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}); });
this.costsVisible = ko.pureComputed(() => { this.costsVisible = ko.pureComputed(() => {
return !this.container.isEmulator; return configContext.platform !== Platform.Emulator;
}); });
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => { this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
@@ -304,23 +305,76 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.formErrors(""); this.formErrors("");
this.isExecuting(true); this.isExecuting(true);
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParameters: DataModels.RpParameters = {
autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(), db: addDatabasePaneStartMessage.database.id,
databaseId: addDatabasePaneStartMessage.database.id, st: addDatabasePaneStartMessage.database.shared,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared, offerThroughput: addDatabasePaneStartMessage.offerThroughput,
offerThroughput: addDatabasePaneStartMessage.offerThroughput sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: addDatabasePaneStartMessage.databaseAccountName
}; };
createDatabase(createDatabaseParams).then( const autopilotSettings = this._getAutopilotSettings();
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, startKey); if (this.container.isPreferredApiCassandra()) {
}, this._createKeyspace(createDatabaseParameters, autopilotSettings, startKey);
(reason: any) => { } else if (this.container.isPreferredApiMongoDB() && EnvironmentUtility.isAadUser()) {
this._onCreateDatabaseFailure(reason, offerThroughput, reason); 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);
});
} }
); );
} }
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() { public resetData() {
this.databaseId(""); this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault()); this.databaseCreateNewShared(this.getSharedThroughputDefault());
@@ -343,6 +397,72 @@ export default class AddDatabasePane extends ContextualPaneBase {
return true; 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 { private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
this.isExecuting(false); this.isExecuting(false);
this.close(); this.close();
@@ -463,6 +583,20 @@ export default class AddDatabasePane extends ContextualPaneBase {
return undefined; 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() { private _updateThroughputLimitByDatabase() {
const throughputDefaults = this.container.collectionCreationDefaults.throughput; const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.throughput(throughputDefaults.shared); this.throughput(throughputDefaults.shared);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ export class SplashScreenComponent extends React.Component<SplashScreenComponent
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
<img src={item.iconSrc} alt="" /> <img src={item.iconSrc} alt={item.title} />
<div className="legendContainer"> <div className="legendContainer">
<div className="legend">{item.title}</div> <div className="legend">{item.title}</div>
<div className="description">{item.description}</div> <div className="description">{item.description}</div>
@@ -66,7 +66,7 @@ export class SplashScreenComponent extends React.Component<SplashScreenComponent
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
<img src={item.iconSrc} alt="" /> <img src={item.iconSrc} alt={item.title} />
<span className="oneLineContent" title={item.info}> <span className="oneLineContent" title={item.info}>
{item.title} {item.title}
</span> </span>
@@ -79,7 +79,7 @@ export class SplashScreenComponent extends React.Component<SplashScreenComponent
<ul> <ul>
{this.props.recentItems.map((item: SplashScreenItem, index: number) => ( {this.props.recentItems.map((item: SplashScreenItem, index: number) => (
<li key={`${item.title}${item.description}${index}`}> <li key={`${item.title}${item.description}${index}`}>
<img src={item.iconSrc} alt="" /> <img src={item.iconSrc} alt={item.title} />
<span className="twoLineContent"> <span className="twoLineContent">
<Link onClick={item.onClick} title={item.info}> <Link onClick={item.onClick} title={item.info}>
{item.title} {item.title}

View File

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

View File

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

View File

@@ -27,15 +27,9 @@ describe("Documents tab", () => {
}); });
describe("showPartitionKey", () => { 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); mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{ const collectionWithoutPartitionKey = <ViewModels.Collection>(<unknown>{

View File

@@ -147,30 +147,6 @@ export default class NotebookTabV2 extends TabsBase {
const cellCodeType = "code"; const cellCodeType = "code";
const cellMarkdownType = "markdown"; const cellMarkdownType = "markdown";
const cellRawType = "raw"; 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[] = [ let buttons: CommandButtonComponentProps[] = [
{ {
iconSrc: SaveIcon, iconSrc: SaveIcon,
@@ -180,7 +156,8 @@ export default class NotebookTabV2 extends TabsBase {
hasPopup: false, hasPopup: false,
disabled: false, disabled: false,
ariaLabel: saveLabel, ariaLabel: saveLabel,
children: saveButtonChildren.length && [ children: this.container.isGalleryPublishEnabled()
? [
{ {
iconName: "Save", iconName: "Save",
onCommandClick: () => this.notebookComponentAdapter.notebookSave(), onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
@@ -189,8 +166,24 @@ export default class NotebookTabV2 extends TabsBase {
disabled: false, disabled: false,
ariaLabel: saveLabel ariaLabel: saveLabel
}, },
...saveButtonChildren {
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
}, },
{ {
iconSrc: null, iconSrc: null,

View File

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

View File

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

View File

@@ -17,11 +17,12 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { PlatformType } from "../../PlatformType"; import { PlatformType } from "../../PlatformType";
import { RequestOptions } from "@azure/cosmos/dist-esm"; import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { updateOffer } from "../../Common/DocumentClientUtilityBase"; import { updateOffer, updateCollection } from "../../Common/DocumentClientUtilityBase";
import { updateCollection } from "../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit"; import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
import { config } from "process";
import { configContext, Platform } from "../../ConfigContext";
const ttlWarning: string = ` 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. 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.
@@ -455,7 +456,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}); });
this.rupmVisible = ko.computed(() => { this.rupmVisible = ko.computed(() => {
if (this.container.isEmulator) { if (configContext.platform === Platform.Emulator) {
return false; return false;
} }
if (this.container.isFeatureEnabled(Constants.Features.enableRupm)) { if (this.container.isFeatureEnabled(Constants.Features.enableRupm)) {
@@ -485,7 +486,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}); });
this.costsVisible = ko.computed(() => { this.costsVisible = ko.computed(() => {
return !this.container.isEmulator; return configContext.platform !== Platform.Emulator;
}); });
this.isTryCosmosDBSubscription = ko.computed<boolean>(() => { this.isTryCosmosDBSubscription = ko.computed<boolean>(() => {
@@ -501,7 +502,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}); });
this.canRequestSupport = ko.pureComputed(() => { this.canRequestSupport = ko.pureComputed(() => {
if (this.container.isEmulator) { if (configContext.platform === Platform.Emulator) {
return false; return false;
} }
@@ -708,7 +709,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
} }
const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs(); const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs();
const isEmulator = this.container.isEmulator; const isEmulator = configContext.platform === Platform.Emulator;
if (isThroughputGreaterThanMaxRus && isEmulator) { if (isThroughputGreaterThanMaxRus && isEmulator) {
return false; return false;
} }
@@ -879,7 +880,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million && this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million; this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
const throughputExceedsMaxValue: boolean = !this.container.isEmulator && this.throughput() > this.maxRUs(); const throughputExceedsMaxValue: boolean =
configContext.platform !== Platform.Emulator && this.throughput() > this.maxRUs();
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty(); const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
const ttlOrIndexingPolicyFieldsDirty: boolean = const ttlOrIndexingPolicyFieldsDirty: boolean =
@@ -1010,7 +1012,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
); );
} }
public onSaveClick = async (): Promise<any> => { public onSaveClick = (): Q.Promise<any> => {
let promises: Q.Promise<void>[] = [];
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@@ -1023,7 +1026,6 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
const newCollectionAttributes: any = {}; const newCollectionAttributes: any = {};
try {
if (this.shouldUpdateCollection()) { if (this.shouldUpdateCollection()) {
let defaultTtl: number; let defaultTtl: number;
switch (this.timeToLive()) { switch (this.timeToLive()) {
@@ -1065,18 +1067,9 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges; newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
} }
const newCollection: DataModels.Collection = _.extend( const newCollection: DataModels.Collection = _.extend({}, this.collection.rawDataModel, newCollectionAttributes);
{}, const updateCollectionPromise = updateCollection(this.collection.databaseId, this.collection, newCollection).then(
this.collection.rawDataModel, (updatedCollection: DataModels.Collection) => {
newCollectionAttributes
);
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newCollection
);
if (updatedCollection) {
this.collection.rawDataModel = updatedCollection; this.collection.rawDataModel = updatedCollection;
this.collection.defaultTtl(updatedCollection.defaultTtl); this.collection.defaultTtl(updatedCollection.defaultTtl);
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl); this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
@@ -1086,6 +1079,9 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy); this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig); this.collection.geospatialConfig(updatedCollection.geospatialConfig);
} }
);
promises.push(updateCollectionPromise);
} }
if ( if (
@@ -1157,8 +1153,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
throughput: newThroughput, throughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
}; };
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
await updateOfferThroughputBeyondLimit(requestPayload); () => {
this.collection.offer().content.offerThroughput = originalThroughputValue; this.collection.offer().content.offerThroughput = originalThroughputValue;
this.throughput(originalThroughputValue); this.throughput(originalThroughputValue);
this.notificationStatusInfo( this.notificationStatusInfo(
@@ -1172,31 +1168,8 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
) )
); );
this.throughput.valueHasMutated(); // force component re-render 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 (error: any) => {
);
} catch (error) {
this.container.isRefreshingExplorer(false);
this.isExecutionError(true);
console.error(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.UpdateSettings, Action.UpdateSettings,
{ {
@@ -1211,8 +1184,59 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
startKey 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();
}
);
promises.push(updateOfferPromise);
}
}
if (promises.length === 0) {
this.isExecuting(false); 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
);
}
)
.finally(() => this.isExecuting(false));
}; };
public onRevertClick = (): Q.Promise<any> => { public onRevertClick = (): Q.Promise<any> => {

View File

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

View File

@@ -559,7 +559,6 @@ export default class Collection implements ViewModels.Collection {
}); });
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; 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 => { const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
return tab.collection && tab.collection.rid === this.rid; return tab.collection && tab.collection.rid === this.rid;
}); });
@@ -575,9 +574,8 @@ export default class Collection implements ViewModels.Collection {
tabTitle: tabTitle tabTitle: tabTitle
}); });
Q.all([pendingNotificationsPromise, this.readSettings()]).then( Q.all([this.readSettings()]).then(
(data: any) => { (data: any) => {
const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new SettingsTab({ settingsTab = new SettingsTab({
tabKind: ViewModels.CollectionTabKind.Settings, tabKind: ViewModels.CollectionTabKind.Settings,
title: !this.offer() ? "Settings" : "Scale & Settings", title: !this.offer() ? "Settings" : "Scale & Settings",
@@ -592,7 +590,6 @@ export default class Collection implements ViewModels.Collection {
onUpdateTabsButtons: this.container.onUpdateTabsButtons onUpdateTabsButtons: this.container.onUpdateTabsButtons
}); });
this.container.tabsManager.activateNewTab(settingsTab); this.container.tabsManager.activateNewTab(settingsTab);
settingsTab.pendingNotification(pendingNotification);
}, },
(error: any) => { (error: any) => {
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
@@ -616,16 +613,7 @@ export default class Collection implements ViewModels.Collection {
} }
); );
} else { } else {
pendingNotificationsPromise.then(
(pendingNotification: DataModels.Notification) => {
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab); this.container.tabsManager.activateTab(settingsTab);
},
(error: any) => {
settingsTab.pendingNotification(undefined);
this.container.tabsManager.activateTab(settingsTab);
}
);
} }
}; };
@@ -648,9 +636,7 @@ export default class Collection implements ViewModels.Collection {
}); });
// TODO: Use the collection entity cache to get quota info // TODO: Use the collection entity cache to get quota info
const quotaInfoPromise: Q.Promise<DataModels.CollectionQuotaInfo> = readCollectionQuotaInfo(this); 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( Q.all([quotaInfoPromise, offerInfoPromise]).then(
() => { () => {
this.container.isRefreshingExplorer(false); this.container.isRefreshingExplorer(false);
@@ -659,7 +645,9 @@ export default class Collection implements ViewModels.Collection {
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy"); const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel); const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel);
if (!collectionOffer) { const isDatabaseShared = this.getDatabase() && this.getDatabase().isDatabaseShared();
const isServerless = this.container.isServerlessEnabled();
if ((isDatabaseShared || isServerless) && !collectionOffer) {
this.quotaInfo(quotaInfo); this.quotaInfo(quotaInfo);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadOffers, Action.LoadOffers,
@@ -1283,48 +1271,6 @@ export default class Collection implements ViewModels.Collection {
return deferred.promise; 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 { private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data; const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
const numFiles: number = uploadDetailsRecords.length; const numFiles: number = uploadDetailsRecords.length;

View File

@@ -52,7 +52,6 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree dataExplorerArea: Constants.Areas.ResourceTree
}); });
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs( const matchingTabs = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.DatabaseSettings, ViewModels.CollectionTabKind.DatabaseSettings,
tab => tab.rid === this.rid tab => tab.rid === this.rid
@@ -66,9 +65,8 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale" tabTitle: "Scale"
}); });
Q.all([pendingNotificationsPromise, this.readSettings()]).then( Q.all([this.readSettings()]).then(
(data: any) => { () => {
const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new DatabaseSettingsTab({ settingsTab = new DatabaseSettingsTab({
tabKind: ViewModels.CollectionTabKind.DatabaseSettings, tabKind: ViewModels.CollectionTabKind.DatabaseSettings,
title: "Scale", title: "Scale",
@@ -83,7 +81,6 @@ export default class Database implements ViewModels.Database {
onUpdateTabsButtons: this.container.onUpdateTabsButtons onUpdateTabsButtons: this.container.onUpdateTabsButtons
}); });
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateNewTab(settingsTab); this.container.tabsManager.activateNewTab(settingsTab);
}, },
(error: any) => { (error: any) => {
@@ -108,21 +105,16 @@ export default class Database implements ViewModels.Database {
} }
); );
} else { } else {
pendingNotificationsPromise.then(
(pendingNotification: DataModels.Notification) => {
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab); this.container.tabsManager.activateTab(settingsTab);
},
(error: any) => {
settingsTab.pendingNotification(undefined);
this.container.tabsManager.activateTab(settingsTab);
}
);
} }
}; };
public readSettings(): Q.Promise<void> { public readSettings(): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>(); const deferred: Q.Deferred<void> = Q.defer<void>();
if (this.container.isServerlessEnabled()) {
deferred.resolve();
}
this.container.isRefreshingExplorer(true); this.container.isRefreshingExplorer(true);
const databaseDataModel: DataModels.Database = <DataModels.Database>{ const databaseDataModel: DataModels.Database = <DataModels.Database>{
id: this.id(), id: this.id(),
@@ -134,9 +126,7 @@ export default class Database implements ViewModels.Database {
defaultExperience: this.container.defaultExperience() 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( Q.all([offerInfoPromise]).then(
() => { () => {
this.container.isRefreshingExplorer(false); this.container.isRefreshingExplorer(false);
@@ -145,11 +135,6 @@ export default class Database implements ViewModels.Database {
offerInfoPromise.valueOf(), offerInfoPromise.valueOf(),
databaseDataModel databaseDataModel
); );
if (!databaseOffer) {
return;
}
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => { readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
const offerThroughputInfo: DataModels.OfferThroughputInfo = { const offerThroughputInfo: DataModels.OfferThroughputInfo = {
minimumRUForCollection: minimumRUForCollection:
@@ -296,49 +281,6 @@ export default class Database implements ViewModels.Database {
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId); 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( private getDeltaCollections(
updatedCollectionsList: DataModels.Collection[] updatedCollectionsList: DataModels.Collection[]
): { toAdd: DataModels.Collection[]; toDelete: Collection[] } { ): { toAdd: DataModels.Collection[]; toDelete: Collection[] } {

View File

@@ -546,13 +546,8 @@ export class ResourceTreeAdapter implements ReactAdapter {
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: createFileContextMenu && this.createFileContextMenu(item), contextMenu: createFileContextMenu
data: item ? [
};
}
private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
let items: TreeNodeMenuItem[] = [
{ {
label: "Rename", label: "Rename",
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
@@ -582,14 +577,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
iconSrc: NotebookIcon, iconSrc: NotebookIcon,
onClick: () => this.container.downloadFile(item) onClick: () => this.container.downloadFile(item)
} }
]; ]
: undefined,
// "Copy to ..." isn't needed if github locations are not available data: item
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { };
items = items.filter(item => item.label !== "Copy to ...");
}
return items;
} }
private copyNotebook = async (item: NotebookContentItem) => { private copyNotebook = async (item: NotebookContentItem) => {

View File

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

View File

@@ -1,6 +1,24 @@
import EmulatorExplorerFactory from "./ExplorerFactory";
import Explorer from "../../Explorer/Explorer"; import Explorer from "../../Explorer/Explorer";
import { AccountKind, DefaultAccountExperience, TagNames } from "../../Common/Constants";
export function initializeExplorer(): Explorer { export function initializeExplorer(): Explorer {
return EmulatorExplorerFactory.createExplorer(); 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;
} }

View File

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

View File

@@ -1,9 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,13 @@ export async function update(
body: Types.DatabaseAccountUpdateParameters body: Types.DatabaseAccountUpdateParameters
): Promise<Types.DatabaseAccountGetResults> { ): Promise<Types.DatabaseAccountGetResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PATCH", apiVersion, body }); return armRequest({
host: configContext.ARM_ENDPOINT,
path,
method: "PATCH",
apiVersion,
body: JSON.stringify(body)
});
} }
/* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */ /* Creates or updates an Azure Cosmos DB database account. The "Update" method is preferred when performing updates on an account. */
@@ -38,7 +44,7 @@ export async function createOrUpdate(
body: Types.DatabaseAccountCreateUpdateParameters body: Types.DatabaseAccountCreateUpdateParameters
): Promise<Types.DatabaseAccountGetResults> { ): Promise<Types.DatabaseAccountGetResults> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB database account. */ /* Deletes an existing Azure Cosmos DB database account. */
@@ -55,7 +61,7 @@ export async function failoverPriorityChange(
body: Types.FailoverPolicies body: Types.FailoverPolicies
): Promise<void> { ): Promise<void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/failoverPriorityChange`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/failoverPriorityChange`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(body) });
} }
/* Lists all the Azure Cosmos DB database accounts available under the subscription. */ /* Lists all the Azure Cosmos DB database accounts available under the subscription. */
@@ -101,7 +107,7 @@ export async function offlineRegion(
body: Types.RegionForOnlineOffline body: Types.RegionForOnlineOffline
): Promise<void | Types.ErrorResponse> { ): Promise<void | Types.ErrorResponse> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/offlineRegion`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/offlineRegion`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(body) });
} }
/* Online the specified region for the specified Azure Cosmos DB database account. */ /* Online the specified region for the specified Azure Cosmos DB database account. */
@@ -112,7 +118,7 @@ export async function onlineRegion(
body: Types.RegionForOnlineOffline body: Types.RegionForOnlineOffline
): Promise<void | Types.ErrorResponse> { ): Promise<void | Types.ErrorResponse> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/onlineRegion`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/onlineRegion`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(body) });
} }
/* Lists the read-only access keys for the specified Azure Cosmos DB database account. */ /* Lists the read-only access keys for the specified Azure Cosmos DB database account. */
@@ -143,7 +149,7 @@ export async function regenerateKey(
body: Types.DatabaseAccountRegenerateKeyParameters body: Types.DatabaseAccountRegenerateKeyParameters
): Promise<void> { ): Promise<void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/regenerateKey`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/regenerateKey`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "POST", apiVersion, body: JSON.stringify(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. */ /* 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 body: Types.GremlinDatabaseCreateUpdateParameters
): Promise<Types.GremlinDatabaseGetResults | void> { ): Promise<Types.GremlinDatabaseGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB Gremlin database. */ /* Deletes an existing Azure Cosmos DB Gremlin database. */
@@ -73,7 +73,7 @@ export async function updateGremlinDatabaseThroughput(
body: Types.ThroughputSettingsUpdateParameters body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> { ): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/throughputSettings/default`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Migrate an Azure Cosmos DB Gremlin database from manual throughput to autoscale */ /* Migrate an Azure Cosmos DB Gremlin database from manual throughput to autoscale */
@@ -131,7 +131,7 @@ export async function createUpdateGremlinGraph(
body: Types.GremlinGraphCreateUpdateParameters body: Types.GremlinGraphCreateUpdateParameters
): Promise<Types.GremlinGraphGetResults | void> { ): Promise<Types.GremlinGraphGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/graphs/${graphName}`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB Gremlin graph. */ /* Deletes an existing Azure Cosmos DB Gremlin graph. */
@@ -168,7 +168,7 @@ export async function updateGremlinGraphThroughput(
body: Types.ThroughputSettingsUpdateParameters body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> { ): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/gremlinDatabases/${databaseName}/graphs/${graphName}/throughputSettings/default`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Migrate an Azure Cosmos DB Gremlin graph from manual throughput to autoscale */ /* 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 body: Types.MongoDBDatabaseCreateUpdateParameters
): Promise<Types.MongoDBDatabaseGetResults | void> { ): Promise<Types.MongoDBDatabaseGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB MongoDB database. */ /* Deletes an existing Azure Cosmos DB MongoDB database. */
@@ -73,7 +73,7 @@ export async function updateMongoDBDatabaseThroughput(
body: Types.ThroughputSettingsUpdateParameters body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> { ): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/throughputSettings/default`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Migrate an Azure Cosmos DB MongoDB database from manual throughput to autoscale */ /* Migrate an Azure Cosmos DB MongoDB database from manual throughput to autoscale */
@@ -131,7 +131,7 @@ export async function createUpdateMongoDBCollection(
body: Types.MongoDBCollectionCreateUpdateParameters body: Types.MongoDBCollectionCreateUpdateParameters
): Promise<Types.MongoDBCollectionGetResults | void> { ): Promise<Types.MongoDBCollectionGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/collections/${collectionName}`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB MongoDB Collection. */ /* Deletes an existing Azure Cosmos DB MongoDB Collection. */
@@ -168,7 +168,7 @@ export async function updateMongoDBCollectionThroughput(
body: Types.ThroughputSettingsUpdateParameters body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> { ): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/mongodbDatabases/${databaseName}/collections/${collectionName}/throughputSettings/default`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Migrate an Azure Cosmos DB MongoDB collection from manual throughput to autoscale */ /* 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 body: Types.SqlDatabaseCreateUpdateParameters
): Promise<Types.SqlDatabaseGetResults | void> { ): Promise<Types.SqlDatabaseGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB SQL database. */ /* Deletes an existing Azure Cosmos DB SQL database. */
@@ -73,7 +73,7 @@ export async function updateSqlDatabaseThroughput(
body: Types.ThroughputSettingsUpdateParameters body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> { ): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/throughputSettings/default`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Migrate an Azure Cosmos DB SQL database from manual throughput to autoscale */ /* Migrate an Azure Cosmos DB SQL database from manual throughput to autoscale */
@@ -131,7 +131,7 @@ export async function createUpdateSqlContainer(
body: Types.SqlContainerCreateUpdateParameters body: Types.SqlContainerCreateUpdateParameters
): Promise<Types.SqlContainerGetResults | void> { ): Promise<Types.SqlContainerGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB SQL container. */ /* Deletes an existing Azure Cosmos DB SQL container. */
@@ -168,7 +168,7 @@ export async function updateSqlContainerThroughput(
body: Types.ThroughputSettingsUpdateParameters body: Types.ThroughputSettingsUpdateParameters
): Promise<Types.ThroughputSettingsGetResults | void> { ): Promise<Types.ThroughputSettingsGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/throughputSettings/default`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Migrate an Azure Cosmos DB SQL container from manual throughput to autoscale */ /* Migrate an Azure Cosmos DB SQL container from manual throughput to autoscale */
@@ -231,7 +231,7 @@ export async function createUpdateSqlStoredProcedure(
body: Types.SqlStoredProcedureCreateUpdateParameters body: Types.SqlStoredProcedureCreateUpdateParameters
): Promise<Types.SqlStoredProcedureGetResults | void> { ): Promise<Types.SqlStoredProcedureGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/storedProcedures/${storedProcedureName}`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB SQL storedProcedure. */ /* Deletes an existing Azure Cosmos DB SQL storedProcedure. */
@@ -283,7 +283,7 @@ export async function createUpdateSqlUserDefinedFunction(
body: Types.SqlUserDefinedFunctionCreateUpdateParameters body: Types.SqlUserDefinedFunctionCreateUpdateParameters
): Promise<Types.SqlUserDefinedFunctionGetResults | void> { ): Promise<Types.SqlUserDefinedFunctionGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/userDefinedFunctions/${userDefinedFunctionName}`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB SQL userDefinedFunction. */ /* Deletes an existing Azure Cosmos DB SQL userDefinedFunction. */
@@ -335,7 +335,7 @@ export async function createUpdateSqlTrigger(
body: Types.SqlTriggerCreateUpdateParameters body: Types.SqlTriggerCreateUpdateParameters
): Promise<Types.SqlTriggerGetResults | void> { ): Promise<Types.SqlTriggerGetResults | void> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases/${databaseName}/containers/${containerName}/triggers/${triggerName}`; 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 }); return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: JSON.stringify(body) });
} }
/* Deletes an existing Azure Cosmos DB SQL trigger. */ /* Deletes an existing Azure Cosmos DB SQL trigger. */

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,6 @@
import { armRequest } from "./request"; import { armRequest } from "./request";
import { updateUserContext } from "../../UserContext";
describe("ARM request", () => { describe("ARM request", () => {
beforeAll(() => {
updateUserContext({
authorizationToken: "foo"
});
});
it("should call window.fetch", async () => { it("should call window.fetch", async () => {
window.fetch = jest.fn().mockResolvedValue({ window.fetch = jest.fn().mockResolvedValue({
ok: true, ok: true,

View File

@@ -6,9 +6,15 @@ Instead, generate ARM clients that consume this function with stricter typing.
*/ */
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { ErrorResponse } from "./generatedClients/2020-04-01/types";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
interface ErrorResponse {
error: {
code: string;
message: string;
};
}
interface ARMError extends Error { interface ARMError extends Error {
code: string; code: string;
} }
@@ -24,22 +30,18 @@ interface Options {
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them. // TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
export async function armRequest<T>({ host, path, apiVersion, method, body: requestBody }: Options): Promise<T> { export async function armRequest<T>({ host, path, apiVersion, method, body: requestBody }: Options): Promise<T> {
const url = new URL(path, host); const url = new URL(path, host);
const authHeader = userContext.authorizationToken;
if (!authHeader) {
throw new Error("No ARM authorization header provided");
}
url.searchParams.append("api-version", apiVersion); url.searchParams.append("api-version", apiVersion);
const response = await window.fetch(url.href, { const response = await window.fetch(url.href, {
method, method,
headers: { headers: {
Authorization: authHeader Authorization: userContext.authorizationToken
}, },
body: requestBody ? JSON.stringify(requestBody) : undefined body: requestBody ? JSON.stringify(requestBody) : undefined
}); });
if (!response.ok) { if (!response.ok) {
const errorResponse = (await response.json()) as ErrorResponse; const errorResponse = (await response.json()) as ErrorResponse;
const error = new Error(errorResponse.message) as ARMError; const error = new Error(errorResponse.error?.message) as ARMError;
error.code = errorResponse.code; error.code = errorResponse.error.code;
throw error; throw error;
} }
@@ -75,19 +77,15 @@ interface OperationResponse {
} }
async function getOperationStatus(operationStatusUrl: string) { async function getOperationStatus(operationStatusUrl: string) {
const authHeader = userContext.authorizationToken;
if (!authHeader) {
throw new Error("No ARM authorization header provided");
}
const response = await window.fetch(operationStatusUrl, { const response = await window.fetch(operationStatusUrl, {
headers: { headers: {
Authorization: authHeader Authorization: userContext.authorizationToken
} }
}); });
if (!response.ok) { if (!response.ok) {
const errorResponse = (await response.json()) as ErrorResponse; const errorResponse = (await response.json()) as ErrorResponse;
const error = new Error(errorResponse.message) as ARMError; const error = new Error(errorResponse.error?.message) as ARMError;
error.code = errorResponse.code; error.code = errorResponse.error.code;
throw new AbortError(error); throw new AbortError(error);
} }
const body = (await response.json()) as OperationResponse; const body = (await response.json()) as OperationResponse;

View File

@@ -106,7 +106,6 @@
click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }" click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabindex="0" tabindex="0"
aria-label="Refresh tree" aria-label="Refresh tree"
title="Refresh tree"
> >
<img <img
class="refreshcol" class="refreshcol"
@@ -122,7 +121,6 @@
click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }" click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }"
tabindex="0" tabindex="0"
aria-label="Collapse Tree" aria-label="Collapse Tree"
title="Collapse Tree"
> >
<img class="refreshcol1" src="/imgarrowlefticon.svg" alt="Hide" /> <img class="refreshcol1" src="/imgarrowlefticon.svg" alt="Hide" />
</span> </span>
@@ -261,7 +259,7 @@
<div class="splashLoaderContentContainer"> <div class="splashLoaderContentContainer">
<p class="connectExplorerContent"><img src="/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" /></p> <p class="connectExplorerContent"><img src="/HdeConnectCosmosDB.svg" alt="Azure Cosmos DB" /></p>
<p class="splashLoaderTitle" id="explorerLoadingStatusTitle">Welcome to Azure Cosmos DB</p> <p class="splashLoaderTitle" id="explorerLoadingStatusTitle">Welcome to Azure Cosmos DB</p>
<p class="splashLoaderText" id="explorerLoadingStatusText" role="alert">Connecting...</p> <p class="splashLoaderText" id="explorerLoadingStatusText">Connecting...</p>
</div> </div>
</div> </div>
<!-- Global loader - End --> <!-- Global loader - End -->

View File

@@ -72,26 +72,7 @@
"./src/Utils/MessageValidation.ts", "./src/Utils/MessageValidation.ts",
"./src/Utils/OfferUtils.ts", "./src/Utils/OfferUtils.ts",
"./src/Utils/StringUtils.ts", "./src/Utils/StringUtils.ts",
"./src/Utils/arm/generatedClients/2020-04-01/cassandraResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collection.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collectionPartition.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collectionPartitionRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collectionRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/database.ts",
"./src/Utils/arm/generatedClients/2020-04-01/databaseAccountRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/databaseAccounts.ts",
"./src/Utils/arm/generatedClients/2020-04-01/gremlinResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/mongoDBResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/operations.ts",
"./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeId.ts",
"./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeIdRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/percentile.ts",
"./src/Utils/arm/generatedClients/2020-04-01/percentileSourceTarget.ts",
"./src/Utils/arm/generatedClients/2020-04-01/percentileTarget.ts",
"./src/Utils/arm/generatedClients/2020-04-01/sqlResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/tableResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/types.ts", "./src/Utils/arm/generatedClients/2020-04-01/types.ts",
"./src/Utils/arm/request.ts",
"./src/quickstart.ts", "./src/quickstart.ts",
"./src/setupTests.ts", "./src/setupTests.ts",
"./src/workers/upload/definitions.ts" "./src/workers/upload/definitions.ts"

View File

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