Compare commits

..

1 Commits

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

View File

@@ -266,6 +266,10 @@ src/ResourceProvider/ResourceProviderClientFactory.ts
src/RouteHandlers/RouteHandler.ts src/RouteHandlers/RouteHandler.ts
src/RouteHandlers/TabRouteHandler.test.ts src/RouteHandlers/TabRouteHandler.test.ts
src/RouteHandlers/TabRouteHandler.ts src/RouteHandlers/TabRouteHandler.ts
src/Shared/AddCollectionUtility.test.ts
src/Shared/AddCollectionUtility.ts
src/Shared/AddDatabaseUtility.test.ts
src/Shared/AddDatabaseUtility.ts
src/Shared/Constants.ts src/Shared/Constants.ts
src/Shared/DefaultExperienceUtility.test.ts src/Shared/DefaultExperienceUtility.test.ts
src/Shared/DefaultExperienceUtility.ts src/Shared/DefaultExperienceUtility.ts
@@ -275,6 +279,8 @@ src/Shared/StorageUtility.test.ts
src/Shared/StorageUtility.ts src/Shared/StorageUtility.ts
src/Shared/StringUtility.test.ts src/Shared/StringUtility.test.ts
src/Shared/StringUtility.ts src/Shared/StringUtility.ts
src/Shared/Telemetry/TelemetryConstants.ts
src/Shared/Telemetry/TelemetryProcessor.ts
src/Shared/appInsights.ts src/Shared/appInsights.ts
src/SparkClusterManager/ArcadiaResourceManager.ts src/SparkClusterManager/ArcadiaResourceManager.ts
src/SparkClusterManager/SparkClusterManager.ts src/SparkClusterManager/SparkClusterManager.ts
@@ -412,5 +418,6 @@ cypress/integration/dataexplorer/SQL/addCollection.spec.ts
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
cypress/integration/notebook/newNotebook.spec.ts cypress/integration/notebook/newNotebook.spec.ts
cypress/integration/notebook/resourceTree.spec.ts cypress/integration/notebook/resourceTree.spec.ts
__mocks__/AddDatabaseUtility.ts
__mocks__/monaco-editor.ts __mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx

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

@@ -0,0 +1,5 @@
export class AddDbUtilities {
createGremlinDatabase(params: any) {
return Promise.resolve(1)
}
}

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": {

View File

@@ -3018,7 +3018,3 @@ settings-pane {
.italic { .italic {
font-style: italic; font-style: italic;
} }
.warningErrorContent a {
color: @AccentMediumHigh
}

730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,11 @@
"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",
"@microsoft/applicationinsights-web": "2.5.8", "@microsoft/applicationinsights-web": "2.5.4",
"@nteract/commutable": "7.1.4", "@nteract/commutable": "7.1.4",
"@nteract/connected-components": "6.7.8", "@nteract/connected-components": "6.7.8",
"@nteract/core": "13.0.0", "@nteract/core": "13.0.0",
@@ -66,13 +66,13 @@
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.15.6", "monaco-editor": "0.15.6",
"object.entries": "1.1.0", "object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1", "office-ui-fabric-react": "7.121.10",
"p-retry": "4.2.0", "p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"promise-polyfill": "8.1.0", "promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0", "promise.prototype.finally": "3.1.0",
"q": "1.5.1", "q": "1.5.1",
"react": "16.13.1", "react": "16.9.0",
"react-animate-height": "2.0.8", "react-animate-height": "2.0.8",
"react-dnd": "9.4.0", "react-dnd": "9.4.0",
"react-dnd-html5-backend": "9.4.0", "react-dnd-html5-backend": "9.4.0",
@@ -118,8 +118,8 @@
"@types/text-encoding": "0.0.33", "@types/text-encoding": "0.0.33",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@types/webfontloader": "1.6.29", "@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "4.0.1", "@typescript-eslint/eslint-plugin": "3.2.0",
"@typescript-eslint/parser": "4.0.1", "@typescript-eslint/parser": "3.2.0",
"adal-angular": "1.0.15", "adal-angular": "1.0.15",
"axe-puppeteer": "1.1.0", "axe-puppeteer": "1.1.0",
"babel-jest": "24.9.0", "babel-jest": "24.9.0",
@@ -132,7 +132,7 @@
"enzyme": "3.10.0", "enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.1", "enzyme-adapter-react-16": "1.15.1",
"enzyme-to-json": "3.4.3", "enzyme-to-json": "3.4.3",
"eslint": "7.8.1", "eslint": "7.3.1",
"eslint-cli": "1.1.1", "eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2", "eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2", "eslint-plugin-prefer-arrow": "1.2.2",
@@ -160,12 +160,11 @@
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"swr": "0.3.2",
"terser-webpack-plugin": "3.0.5", "terser-webpack-plugin": "3.0.5",
"ts-loader": "6.2.2", "ts-loader": "6.2.2",
"tslint": "5.11.0", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0", "tslint-microsoft-contrib": "6.0.0",
"typescript": "4.0.2", "typescript": "3.9.6",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"webpack": "4.43.0", "webpack": "4.43.0",
"webpack-bundle-analyzer": "3.6.1", "webpack-bundle-analyzer": "3.6.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

@@ -5,7 +5,14 @@ import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient"; import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient"; import {
deleteDocument,
getEndpoint,
queryDocuments,
readDocument,
updateDocument,
_createMongoCollectionWithARM
} from "./MongoProxyClient";
jest.mock("../ResourceProvider/ResourceProviderClient.ts"); jest.mock("../ResourceProvider/ResourceProviderClient.ts");
const databaseId = "testDB"; const databaseId = "testDB";
@@ -253,4 +260,58 @@ describe("MongoProxyClient", () => {
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
}); });
}); });
describe("createMongoCollectionWithARM", () => {
it("should create a collection with autopilot when autopilot is selected + shared throughput is false", () => {
const resourceProviderClientPutAsyncSpy = jest.spyOn(ResourceProviderClient.prototype, "putAsync");
const properties = {
pk: "state",
coll: "abc-collection",
cd: true,
db: "a1-db",
st: false,
sid: "a2",
rg: "c1",
dba: "main",
is: false
};
_createMongoCollectionWithARM("management.azure.com", properties, { "x-ms-cosmos-offer-autopilot-tier": "1" });
expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith(
"subscriptions/a2/resourceGroups/c1/providers/Microsoft.DocumentDB/databaseAccounts/foo/mongodbDatabases/a1-db/collections/abc-collection",
"2020-04-01",
{
properties: {
options: { "x-ms-cosmos-offer-autopilot-tier": "1" },
resource: { id: "abc-collection" }
}
}
);
});
it("should create a collection with provisioned throughput when provisioned throughput is selected + shared throughput is false", () => {
const resourceProviderClientPutAsyncSpy = jest.spyOn(ResourceProviderClient.prototype, "putAsync");
const properties = {
pk: "state",
coll: "abc-collection",
cd: true,
db: "a1-db",
st: false,
sid: "a2",
rg: "c1",
dba: "main",
is: false,
offerThroughput: 400
};
_createMongoCollectionWithARM("management.azure.com", properties, undefined);
expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith(
"subscriptions/a2/resourceGroups/c1/providers/Microsoft.DocumentDB/databaseAccounts/foo/mongodbDatabases/a1-db/collections/abc-collection",
"2020-04-01",
{
properties: {
options: { throughput: "400" },
resource: { id: "abc-collection" }
}
}
);
});
});
}); });

View File

@@ -1,12 +1,16 @@
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 { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
@@ -281,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: {
@@ -323,10 +335,52 @@ 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);
}); });
} }
export function createMongoCollectionWithARM(
armEndpoint: string,
databaseId: string,
analyticalStorageTtl: number,
collectionId: string,
offerThroughput: number,
shardKey: string,
createDatabase: boolean,
sharedThroughput: boolean,
isSharded: boolean,
additionalOptions?: DataModels.RpOptions
): Promise<DataModels.CreateCollectionWithRpResponse> {
const databaseAccount = userContext.databaseAccount;
const params: DataModels.MongoParameters = {
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
db: databaseId,
coll: collectionId,
pk: shardKey,
offerThroughput,
cd: createDatabase,
st: sharedThroughput,
is: isSharded,
rid: "",
rtype: "colls",
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: databaseAccount.name,
analyticalStorageTtl
};
if (createDatabase) {
return AddDbUtilities.createMongoDatabaseWithARM(
armEndpoint,
params,
sharedThroughput ? additionalOptions : {}
).then(() => {
return _createMongoCollectionWithARM(armEndpoint, params, sharedThroughput ? {} : additionalOptions);
});
}
return _createMongoCollectionWithARM(armEndpoint, params, additionalOptions);
}
export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string { export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
const serverId = window.dataExplorer.serverId(); const serverId = window.dataExplorer.serverId();
const extensionEndpoint = window.dataExplorer.extensionEndpoint(); const extensionEndpoint = window.dataExplorer.extensionEndpoint();
@@ -359,3 +413,46 @@ async function errorHandling(response: Response, action: string, params: unknown
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string { export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`; return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
} }
export async function _createMongoCollectionWithARM(
armEndpoint: string,
params: DataModels.MongoParameters,
rpOptions: DataModels.RpOptions
): Promise<DataModels.CreateCollectionWithRpResponse> {
const rpPayloadToCreateCollection: DataModels.MongoCreationRequest = {
properties: {
resource: {
id: params.coll
},
options: {}
}
};
if (params.is) {
rpPayloadToCreateCollection.properties.resource["shardKey"] = { [params.pk]: "Hash" };
}
if (!params.st) {
if (rpOptions) {
rpPayloadToCreateCollection.properties.options = rpOptions;
} else {
rpPayloadToCreateCollection.properties.options["throughput"] =
params.offerThroughput && params.offerThroughput.toString();
}
}
if (params.analyticalStorageTtl) {
rpPayloadToCreateCollection.properties.resource.analyticalStorageTtl = params.analyticalStorageTtl;
}
try {
return new ResourceProviderClient<DataModels.CreateCollectionWithRpResponse>(armEndpoint).putAsync(
getARMCreateCollectionEndpoint(params),
DataExplorerConstants.ArmApiVersions.publicVersion,
rpPayloadToCreateCollection
);
} catch (response) {
errorHandling(response, "creating collection", undefined);
return undefined;
}
}

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,374 +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-cosmos-db/types";
import { client } from "../CosmosClient";
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
import {
createUpdateSqlContainer,
getSqlContainer
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/sqlResources";
import {
createUpdateCassandraTable,
getCassandraTable
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/cassandraResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/mongoDBResources";
import {
createUpdateGremlinGraph,
getGremlinGraph
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/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,254 +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-cosmos-db/types";
import { client } from "../CosmosClient";
import {
createUpdateSqlDatabase,
getSqlDatabase
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/sqlResources";
import {
createUpdateCassandraKeyspace,
getCassandraKeyspace
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/cassandraResources";
import {
createUpdateMongoDBDatabase,
getMongoDBDatabase
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/mongoDBResources";
import {
createUpdateGremlinDatabase,
getGremlinDatabase
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/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

@@ -1,10 +1,10 @@
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/sqlResources"; import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/cassandraResources"; import { deleteCassandraTable } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/mongoDBResources"; import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/gremlinResources"; import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/tableResources"; import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger"; import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError"; import { sendNotificationForError } from "./sendNotificationForError";
@@ -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

@@ -1,9 +1,9 @@
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/sqlResources"; import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/cassandraResources"; import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/mongoDBResources"; import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/gremlinResources"; import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@@ -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

@@ -2,11 +2,11 @@ import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/sqlResources"; import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/cassandraResources"; import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/mongoDBResources"; import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/gremlinResources"; import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/tableResources"; import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger"; import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError"; import { sendNotificationForError } from "./sendNotificationForError";
@@ -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

@@ -2,10 +2,10 @@ import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/sqlResources"; import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/cassandraResources"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/mongoDBResources"; import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/gremlinResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger"; import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError"; import { sendNotificationForError } from "./sendNotificationForError";
@@ -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,228 +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-cosmos-db/types";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import {
createUpdateSqlContainer,
getSqlContainer
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/sqlResources";
import {
createUpdateCassandraTable,
getCassandraTable
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/cassandraResources";
import {
createUpdateMongoDBCollection,
getMongoDBCollection
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/mongoDBResources";
import {
createUpdateGremlinGraph,
getGremlinGraph
} from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01-cosmos-db/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 {
@@ -709,6 +690,11 @@ export interface SparkPool extends ArmResource {
properties: SparkPoolProperties; properties: SparkPoolProperties;
} }
export interface MemoryUsageInfo {
freeKB: number;
totalKB: number;
}
export interface resourceTokenConnectionStringProperties { export interface resourceTokenConnectionStringProperties {
accountEndpoint: string; accountEndpoint: string;
collectionId: string; collectionId: string;

View File

@@ -1,6 +1,6 @@
import { StringUtils } from "../../../Utils/StringUtils"; import { StringUtils } from "../../../Utils/StringUtils";
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
import * as 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 CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png"; import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";

View File

@@ -265,9 +265,6 @@ exports[`test render renders with filters 1`] = `
"buttonTextDisabled": "#a19f9d", "buttonTextDisabled": "#a19f9d",
"buttonTextHovered": "#201f1e", "buttonTextHovered": "#201f1e",
"buttonTextPressed": "#201f1e", "buttonTextPressed": "#201f1e",
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"cardStandoutBackground": "#ffffff",
"defaultStateBackground": "#faf9f8", "defaultStateBackground": "#faf9f8",
"disabledBackground": "#f3f2f1", "disabledBackground": "#f3f2f1",
"disabledBodySubtext": "#c8c6c4", "disabledBodySubtext": "#c8c6c4",
@@ -607,9 +604,6 @@ exports[`test render renders with filters 1`] = `
"buttonTextDisabled": "#a19f9d", "buttonTextDisabled": "#a19f9d",
"buttonTextHovered": "#201f1e", "buttonTextHovered": "#201f1e",
"buttonTextPressed": "#201f1e", "buttonTextPressed": "#201f1e",
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"cardStandoutBackground": "#ffffff",
"defaultStateBackground": "#faf9f8", "defaultStateBackground": "#faf9f8",
"disabledBackground": "#f3f2f1", "disabledBackground": "#f3f2f1",
"disabledBodySubtext": "#c8c6c4", "disabledBodySubtext": "#c8c6c4",
@@ -1003,9 +997,6 @@ exports[`test render renders with filters 1`] = `
"buttonTextDisabled": "#a19f9d", "buttonTextDisabled": "#a19f9d",
"buttonTextHovered": "#201f1e", "buttonTextHovered": "#201f1e",
"buttonTextPressed": "#201f1e", "buttonTextPressed": "#201f1e",
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"cardStandoutBackground": "#ffffff",
"defaultStateBackground": "#faf9f8", "defaultStateBackground": "#faf9f8",
"disabledBackground": "#f3f2f1", "disabledBackground": "#f3f2f1",
"disabledBodySubtext": "#c8c6c4", "disabledBodySubtext": "#c8c6c4",
@@ -1122,11 +1113,6 @@ exports[`test render renders with filters 1`] = `
}, },
"iconDisabled": Object { "iconDisabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object {
"color": "GrayText",
},
},
}, },
"label": Array [ "label": Array [
Object { Object {
@@ -1148,11 +1134,6 @@ exports[`test render renders with filters 1`] = `
}, },
"menuIconDisabled": Object { "menuIconDisabled": Object {
"color": "#a19f9d", "color": "#a19f9d",
"selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object {
"color": "GrayText",
},
},
}, },
"root": Array [ "root": Array [
Object { Object {
@@ -1169,6 +1150,7 @@ exports[`test render renders with filters 1`] = `
"right": 2, "right": 2,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"border": "none",
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
"outlineColor": "ButtonText", "outlineColor": "ButtonText",
@@ -1248,6 +1230,7 @@ exports[`test render renders with filters 1`] = `
"right": 2, "right": 2,
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"border": "none",
"bottom": -2, "bottom": -2,
"left": -2, "left": -2,
"outlineColor": "ButtonText", "outlineColor": "ButtonText",
@@ -1276,6 +1259,10 @@ exports[`test render renders with filters 1`] = `
":hover": Object { ":hover": Object {
"outline": 0, "outline": 0,
}, },
"@media screen and (-ms-high-contrast: active)": Object {
"borderColor": "grayText",
"color": "grayText",
},
}, },
}, },
Object { Object {
@@ -1375,21 +1362,13 @@ exports[`test render renders with filters 1`] = `
"selectors": Object { "selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object { "@media screen and (-ms-high-contrast: active)": Object {
"MsHighContrastAdjust": "none", "MsHighContrastAdjust": "none",
"backgroundColor": "Window", "backgroundColor": "WindowText",
"border": "1px solid WindowText", "color": "Window",
"borderRightWidth": "0",
"color": "WindowText",
}, },
}, },
}, },
".ms-Button--primary + .ms-Button": Object { ".ms-Button--primary + .ms-Button": Object {
"border": "none", "border": "none",
"selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object {
"border": "1px solid WindowText",
"borderLeftWidth": "0",
},
},
}, },
}, },
}, },
@@ -1429,9 +1408,6 @@ exports[`test render renders with filters 1`] = `
"borderColor": "GrayText", "borderColor": "GrayText",
"color": "GrayText", "color": "GrayText",
}, },
"@media screen and (forced-colors: active)": Object {
"forcedColorAdjust": "none",
},
}, },
}, },
"splitButtonContainerFocused": Object { "splitButtonContainerFocused": Object {
@@ -1578,13 +1554,6 @@ exports[`test render renders with filters 1`] = `
}, },
}, },
}, },
".ms-Button-menuIcon": Object {
"selectors": Object {
"@media screen and (-ms-high-contrast: active)": Object {
"color": "GrayText",
},
},
},
":hover": Object { ":hover": Object {
"cursor": "default", "cursor": "default",
}, },
@@ -1806,9 +1775,6 @@ exports[`test render renders with filters 1`] = `
"buttonTextDisabled": "#a19f9d", "buttonTextDisabled": "#a19f9d",
"buttonTextHovered": "#201f1e", "buttonTextHovered": "#201f1e",
"buttonTextPressed": "#201f1e", "buttonTextPressed": "#201f1e",
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"cardStandoutBackground": "#ffffff",
"defaultStateBackground": "#faf9f8", "defaultStateBackground": "#faf9f8",
"disabledBackground": "#f3f2f1", "disabledBackground": "#f3f2f1",
"disabledBodySubtext": "#c8c6c4", "disabledBodySubtext": "#c8c6c4",

View File

@@ -6,7 +6,7 @@ import { RepoListItem } from "./GitHubReposComponent";
import { ChildrenMargin } from "./GitHubStyleConstants"; import { ChildrenMargin } from "./GitHubStyleConstants";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient"; import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import UrlUtility from "../../../Common/UrlUtility"; import UrlUtility from "../../../Common/UrlUtility";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";

View File

@@ -24,7 +24,7 @@ import {
} from "office-ui-fabric-react/lib/utilities/selection/index"; } from "office-ui-fabric-react/lib/utilities/selection/index";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField"; import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png"; import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import { QueriesClient } from "../../../Common/QueriesClient"; import { QueriesClient } from "../../../Common/QueriesClient";

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

@@ -26,7 +26,7 @@ import NewVertexPane from "./Panes/NewVertexPane";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import Q from "q"; import Q from "q";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts"; import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts";
@@ -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;
@@ -243,6 +239,7 @@ export default class Explorer {
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>; public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>; public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Observable<boolean>; public isSynapseLinkUpdating: ko.Observable<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any; // This is dynamically loaded public notebookManager?: any; // This is dynamically loaded
private _panes: ContextualPaneBase[] = []; private _panes: ContextualPaneBase[] = [];
@@ -268,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
}); });
@@ -373,8 +370,7 @@ export default class Explorer {
); );
} }
}); });
this.notificationsClient = options.notificationsClient; this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.isEmulator = options.isEmulator;
this.features = ko.observable(); this.features = ko.observable();
this.serverId = ko.observable<string>(); this.serverId = ko.observable<string>();
@@ -973,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();
}); });
@@ -1477,33 +1469,38 @@ export default class Explorer {
); );
}; };
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() }); if (this.isServerlessEnabled()) {
this._setLoadingStatusText("Fetching offers..."); // Serverless accounts don't support offers call
offerPromise.then( refreshDatabases();
(offers: DataModels.Offer[]) => { } else {
this._setLoadingStatusText("Successfully fetched offers."); const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers();
refreshDatabases(offers); this._setLoadingStatusText("Fetching offers...");
}, offerPromise.then(
error => { (offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Failed to fetch offers."); this._setLoadingStatusText("Successfully fetched offers.");
this.isRefreshingExplorer(false); refreshDatabases(offers);
deferred.reject(error); },
TelemetryProcessor.traceFailure( error => {
Action.LoadDatabases, this._setLoadingStatusText("Failed to fetch offers.");
{ this.isRefreshingExplorer(false);
databaseAccountName: this.databaseAccount().name, deferred.reject(error);
defaultExperience: this.defaultExperience(), TelemetryProcessor.traceFailure(
dataExplorerArea: Constants.Areas.ResourceTree, Action.LoadDatabases,
error: JSON.stringify(error) {
}, databaseAccountName: this.databaseAccount().name,
startKey defaultExperience: this.defaultExperience(),
); dataExplorerArea: Constants.Areas.ResourceTree,
NotificationConsoleUtils.logConsoleMessage( error: JSON.stringify(error)
ConsoleDataType.Error, },
`Error while refreshing databases: ${JSON.stringify(error)}` startKey
); );
} NotificationConsoleUtils.logConsoleMessage(
); ConsoleDataType.Error,
`Error while refreshing databases: ${JSON.stringify(error)}`
);
}
);
}
return deferred.promise.then( return deferred.promise.then(
() => { () => {
@@ -1936,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);
@@ -1951,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,
{ {
@@ -3115,6 +3106,12 @@ export default class Explorer {
} else { } else {
loadingTitle.innerHTML = title; loadingTitle.innerHTML = title;
} }
TelemetryProcessor.trace(
Action.LoadingStatus,
ActionModifiers.Mark,
title !== "Welcome to Azure Cosmos DB" ? `Title: ${title}, Text: ${text}` : text
);
} }
private _openSetupNotebooksPaneForQuickstart(): void { private _openSetupNotebooksPaneForQuickstart(): void {

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

@@ -23,7 +23,7 @@ import { GraphConfig } from "../../Tabs/GraphTab";
import { EditorReact } from "../../Controls/Editor/EditorReact"; import { EditorReact } from "../../Controls/Editor/EditorReact";
import LoadGraphIcon from "../../../../images/LoadGraph.png"; import LoadGraphIcon from "../../../../images/LoadGraph.png";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels"; import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
@@ -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

@@ -82,7 +82,9 @@ export class CommandBarComponentAdapter implements ReactAdapter {
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (this.isNotebookTabActive()) { if (this.isNotebookTabActive()) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker")); uiFabricControlButtons.unshift(
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
);
} }
return ( return (

View File

@@ -2,7 +2,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { PlatformType } from "../../../PlatformType"; import { PlatformType } from "../../../PlatformType";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../../Common/Constants"; import { Areas } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg"; import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
import AddCollectionIcon from "../../../../images/AddCollection.svg"; import AddCollectionIcon from "../../../../images/AddCollection.svg";
@@ -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

@@ -1,14 +1,16 @@
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar"; import _ from "underscore";
import { Dropdown, IDropdownOption, IDropdownStyles } from "office-ui-fabric-react/lib/Dropdown"; import * as React from "react";
import { Observable } from "knockout";
import { IconType } from "office-ui-fabric-react/lib/Icon"; import { IconType } from "office-ui-fabric-react/lib/Icon";
import { IComponentAsProps } from "office-ui-fabric-react/lib/Utilities"; import { IComponentAsProps } from "office-ui-fabric-react/lib/Utilities";
import * as React from "react";
import _ from "underscore";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { StyleConstants } from "../../../Common/Constants"; import { StyleConstants } from "../../../Common/Constants";
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker"; import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { Dropdown, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
import { MemoryTrackerComponent } from "./MemoryTrackerComponent"; import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
/** /**
* Utilities for CommandBar * Utilities for CommandBar
@@ -176,10 +178,10 @@ export class CommandBarUtil {
}; };
} }
public static createMemoryTracker(key: string): ICommandBarItemProps { public static createMemoryTracker(key: string, memoryUsageInfo: Observable<MemoryUsageInfo>): ICommandBarItemProps {
return { return {
key, key,
onRender: () => <MemoryTrackerComponent /> onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
}; };
} }
} }

View File

@@ -1,71 +1,50 @@
import React, { FunctionComponent } from "react"; import * as React from "react";
import useSWR from "swr"; import { Observable, Subscription } from "knockout";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
import { ProgressIndicator } from "office-ui-fabric-react/lib/ProgressIndicator"; import { ProgressIndicator } from "office-ui-fabric-react/lib/ProgressIndicator";
import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner"; import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/Spinner";
import { Stack } from "office-ui-fabric-react/lib/Stack"; import { Stack } from "office-ui-fabric-react/lib/Stack";
import { listConnectionInfo } from "../../../Utils/arm/generatedClients/2020-04-01-notebook/notebookWorkspaces";
import { NotebookWorkspaceConnectionInfoResult } from "../../../Utils/arm/generatedClients/2020-04-01-notebook/types";
import { userContext } from "../../../UserContext";
export interface MemoryUsageInfo { interface MemoryTrackerProps {
total: number; memoryUsageInfo: Observable<MemoryUsageInfo>;
free: number;
} }
const kbInGB = 1048576; export class MemoryTrackerComponent extends React.Component<MemoryTrackerProps> {
private memoryUsageInfoSubscription: Subscription;
const fetchMemoryInfo = async (_key: unknown, connectionInfo: NotebookWorkspaceConnectionInfoResult) => { public componentDidMount(): void {
const response = await fetch(`${connectionInfo.notebookServerEndpoint}/api/metrics/memory`, { this.memoryUsageInfoSubscription = this.props.memoryUsageInfo.subscribe(() => {
method: "GET", this.forceUpdate();
headers: { });
Authorization: `Token ${connectionInfo.authToken}`,
"content-type": "application/json"
}
});
if (!response.ok) {
throw new Error(await response.text());
} }
const memoryUsageInfo = (await response.json()) as MemoryUsageInfo;
return {
totalKB: memoryUsageInfo.total,
freeKB: memoryUsageInfo.free
};
};
export const MemoryTrackerComponent: FunctionComponent = () => { public componentWillUnmount(): void {
const { data: connectionInfo } = useSWR( this.memoryUsageInfoSubscription && this.memoryUsageInfoSubscription.dispose();
[ }
"notebooksConnectionInfo",
userContext.subscriptionId, public render(): JSX.Element {
userContext.resourceGroup, const memoryUsageInfo: MemoryUsageInfo = this.props.memoryUsageInfo();
userContext.databaseAccount.name, if (!memoryUsageInfo) {
"default" return (
], <Stack className="memoryTrackerContainer" horizontal>
(_key, subscriptionId, resourceGroup, accountName, workspace) => <span>Memory</span>
listConnectionInfo(subscriptionId, resourceGroup, accountName, workspace) <Spinner size={SpinnerSize.medium} />
); </Stack>
const { data } = useSWR(connectionInfo ? ["memoryUsage", connectionInfo] : null, fetchMemoryInfo, { );
refreshInterval: 2000 }
});
const totalGB = memoryUsageInfo.totalKB / 1048576;
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
if (!data) {
return ( return (
<Stack className="memoryTrackerContainer" horizontal> <Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span> <span>Memory</span>
<Spinner size={SpinnerSize.medium} /> <ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
</Stack> </Stack>
); );
} }
const totalGB = data.totalKB / kbInGB; }
const usedGB = totalGB - data.freeKB / kbInGB;
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
</Stack>
);
};

View File

@@ -32,7 +32,7 @@ import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux";
import configureStore from "./NotebookComponent/store"; import configureStore from "./NotebookComponent/store";
import { Notification } from "react-notification-system"; import { Notification } from "react-notification-system";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
export type KernelSpecsDisplay = { name: string; displayName: string }; export type KernelSpecsDisplay = { name: string; displayName: string };

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,47 +74,46 @@ describe("Extract kernel from notebook", () => {
}); });
}); });
const initialState = {
app: state.makeAppRecord({
host: state.makeJupyterHostRecord({
type: "jupyter",
token: "eh",
basePath: "/"
})
}),
comms: state.makeCommsRecord(),
config: Immutable.Map({}),
core: state.makeStateRecord({
kernelRef: "fake",
entities: state.makeEntitiesRecord({
contents: state.makeContentsRecord({
byRef: Immutable.Map({
fakeContentRef: state.makeNotebookContentRecord()
})
}),
kernels: state.makeKernelsRecord({
byRef: Immutable.Map({
fake: state.makeRemoteKernelRecord({
type: "websocket",
channels: new Subject<any>(),
kernelSpecName: "fancy",
id: "0"
})
})
})
})
}),
cdb: makeCdbRecord({
databaseAccountName: "dbAccountName",
defaultExperience: "defaultExperience"
})
};
describe("launchWebSocketKernelEpic", () => { describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create"); const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef"; const contentRef = "fakeContentRef";
const kernelRef = "fake"; const kernelRef = "fake";
const initialState = {
app: state.makeAppRecord({
host: state.makeJupyterHostRecord({
type: "jupyter",
token: "eh",
basePath: "/"
})
}),
comms: state.makeCommsRecord(),
config: Immutable.Map({}),
core: state.makeStateRecord({
kernelRef: "fake",
entities: state.makeEntitiesRecord({
contents: state.makeContentsRecord({
byRef: Immutable.Map({
fakeContentRef: state.makeNotebookContentRecord()
})
}),
kernels: state.makeKernelsRecord({
byRef: Immutable.Map({
fake: state.makeRemoteKernelRecord({
type: "websocket",
channels: new Subject<any>(),
kernelSpecName: "fancy",
id: "0"
})
})
})
})
}),
cdb: makeCdbRecord({
databaseAccountName: "dbAccountName",
defaultExperience: "defaultExperience"
})
};
it("launches remote kernels", async () => { 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";
@@ -37,7 +37,7 @@ import * as Constants from "../../../Common/Constants";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions"; import * as CdbActions from "./actions";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
import { CdbAppState } from "./types"; import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils"; import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
@@ -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

@@ -1,7 +1,7 @@
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core"; import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
import { Action } from "redux"; import { Action } from "redux";
import { Areas } from "../../../Common/Constants"; import { Areas } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as cdbActions from "./actions"; import * as cdbActions from "./actions";
import { CdbRecord } from "./types"; import { CdbRecord } from "./types";

View File

@@ -2,13 +2,89 @@
* Notebook container related stuff * Notebook container related stuff
*/ */
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as Constants from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
export class NotebookContainerClient { export class NotebookContainerClient {
private reconnectingNotificationId: string; private reconnectingNotificationId: string;
private isResettingWorkspace: boolean; private isResettingWorkspace: boolean;
constructor(private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>) {} constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
private onConnectionLost: () => void,
private onMemoryUsageInfoUpdate: (update: DataModels.MemoryUsageInfo) => void
) {
if (notebookServerInfo() && notebookServerInfo().notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else {
const subscription = notebookServerInfo.subscribe((newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo && newServerInfo.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
subscription.dispose();
});
}
}
/**
* Heartbeat: each ping schedules another ping
*/
private scheduleHeartbeat(delayMs: number): void {
setTimeout(() => {
this.getMemoryUsage()
.then(memoryUsageInfo => this.onMemoryUsageInfoUpdate(memoryUsageInfo))
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
}, delayMs);
}
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
return Promise.reject(error);
}
if (this.isResettingWorkspace) {
return undefined;
}
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try {
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
method: "GET",
headers: {
Authorization: authToken,
"content-type": "application/json"
}
});
if (response.ok) {
if (this.reconnectingNotificationId) {
NotificationConsoleUtils.clearInProgressMessageWithId(this.reconnectingNotificationId);
this.reconnectingNotificationId = "";
}
const memoryUsageInfo = await response.json();
if (memoryUsageInfo) {
return {
totalKB: memoryUsageInfo.total,
freeKB: memoryUsageInfo.free
};
}
}
return undefined;
} catch (error) {
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
if (!this.reconnectingNotificationId) {
this.reconnectingNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
this.onConnectionLost();
return undefined;
}
}
public async resetWorkspace(): Promise<void> { public async resetWorkspace(): Promise<void> {
this.isResettingWorkspace = true; this.isResettingWorkspace = true;

View File

@@ -9,13 +9,14 @@ import * as Logger from "../../Common/Logger";
import { HttpStatusCodes, Areas } from "../../Common/Constants"; import { HttpStatusCodes, Areas } from "../../Common/Constants";
import { GitHubReposPane } from "../Panes/GitHubReposPane"; import { GitHubReposPane } from "../Panes/GitHubReposPane";
import ko from "knockout"; import ko from "knockout";
import * as 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 { IContentProvider } from "@nteract/core"; import { IContentProvider } from "@nteract/core";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider"; import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { contents } from "rx-jupyter"; import { contents } from "rx-jupyter";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { MemoryUsageInfo } from "../../Contracts/DataModels";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent"; import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
@@ -75,7 +76,11 @@ export default class NotebookManager {
contents.JupyterContentProvider contents.JupyterContentProvider
); );
this.notebookClient = new NotebookContainerClient(this.params.container.notebookServerInfo); this.notebookClient = new NotebookContainerClient(
this.params.container.notebookServerInfo,
() => this.params.container.initNotebooks(this.params.container.databaseAccount()),
(update: MemoryUsageInfo) => this.params.container.memoryUsageInfo(update)
);
this.notebookContentClient = new NotebookContentClient( this.notebookContentClient = new NotebookContentClient(
this.params.container.notebookServerInfo, this.params.container.notebookServerInfo,

View File

@@ -25,7 +25,7 @@
<!-- Add collection header - Start --> <!-- Add collection header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span id="containerTitle" data-bind="text: title"></span> <span id="containerTitle" data-bind="text: title"></span>
<div class="closeImg" id="closeBtnAddCollection" role="button" aria-label="Add collection close pane" <div class="closeImg" id="closeBtnAddCollection" role="button" aria-label="Close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0"> data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0">
<img src="../../../images/close-black.svg" title="Close" alt="Close" /> <img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div> </div>

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 * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import EnvironmentUtility from "../../Common/EnvironmentUtility";
import Q from "q";
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();
@@ -943,7 +1049,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
const defaultThroughput = this.container.collectionCreationDefaults.throughput; const defaultThroughput = this.container.collectionCreationDefaults.throughput;
this.throughputSinglePartition(defaultThroughput.fixed); this.throughputSinglePartition(defaultThroughput.fixed);
this.throughputMultiPartition( this.throughputMultiPartition(
AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container) AddCollectionUtility.Utilities.getMaxThroughput(this.container.collectionCreationDefaults, this.container)
); );
this.throughputDatabase(defaultThroughput.shared); this.throughputDatabase(defaultThroughput.shared);
@@ -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,
@@ -1167,19 +1302,17 @@ export default class AddCollectionPane extends ContextualPaneBase {
private _updateThroughputLimitByCollectionStorage() { private _updateThroughputLimitByCollectionStorage() {
const storage = this.storage(); const storage = this.storage();
const minThroughputRU = const minThroughputRU = AddCollectionUtility.Utilities.getMinRUForStorageOption(
storage === SharedConstants.CollectionCreation.storage10Gb this.container.collectionCreationDefaults,
? SharedConstants.CollectionCreation.DefaultCollectionRUs400 storage
: this.container.collectionCreationDefaults.throughput.unlimitedmin; );
let maxThroughputRU; let maxThroughputRU = AddCollectionUtility.Utilities.getMaxRUForStorageOption(
this.container.collectionCreationDefaults,
storage
);
if (this.isTryCosmosDBSubscription()) { if (this.isTryCosmosDBSubscription()) {
maxThroughputRU = Constants.TryCosmosExperience.maxRU; maxThroughputRU = Constants.TryCosmosExperience.maxRU;
} else {
maxThroughputRU =
storage === SharedConstants.CollectionCreation.storage10Gb
? SharedConstants.CollectionCreation.DefaultCollectionRUs10K
: this.container.collectionCreationDefaults.throughput.unlimitedmax;
} }
this.minThroughputRU(minThroughputRU); this.minThroughputRU(minThroughputRU);

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

@@ -1,3 +1,4 @@
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
@@ -7,11 +8,16 @@ 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 * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import EnvironmentUtility from "../../Common/EnvironmentUtility";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
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 { 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>;
@@ -174,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
) { ) {
@@ -197,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>(() => {
@@ -299,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());
@@ -338,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();
@@ -458,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

@@ -5,7 +5,7 @@ import { Areas } from "../../Common/Constants";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter"; import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab"; import QueryTab from "../Tabs/QueryTab";
export class BrowseQueriesPane extends ContextualPaneBase { export class BrowseQueriesPane extends ContextualPaneBase {

View File

@@ -7,11 +7,12 @@ import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; 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 * as 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 { 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;
} }
@@ -494,7 +495,9 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.selectedSharedAutoPilotTier(null); this.selectedSharedAutoPilotTier(null);
this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container)); this.throughput(
AddCollectionUtility.Utilities.getMaxThroughput(this.container.collectionCreationDefaults, this.container)
);
this.keyspaceThroughput(throughputDefaults.shared); this.keyspaceThroughput(throughputDefaults.shared);
this.maxThroughputRU(throughputDefaults.unlimitedmax); this.maxThroughputRU(throughputDefaults.unlimitedmax);
this.minThroughputRU(throughputDefaults.unlimitedmin); this.minThroughputRU(throughputDefaults.unlimitedmin);

View File

@@ -4,7 +4,7 @@ import * as Constants from "../../Common/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { KeyCodes } from "../../Common/Constants"; import { KeyCodes } from "../../Common/Constants";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data

View File

@@ -8,7 +8,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import DeleteCollectionConfirmationPane from "./DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./DeleteCollectionConfirmationPane";
import DeleteFeedback from "../../Common/DeleteFeedback"; import DeleteFeedback from "../../Common/DeleteFeedback";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { TreeNode } from "../../Contracts/ViewModels"; import { TreeNode } from "../../Contracts/ViewModels";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
@@ -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

@@ -10,7 +10,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import DeleteFeedback from "../../Common/DeleteFeedback"; import DeleteFeedback from "../../Common/DeleteFeedback";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
export default class DeleteCollectionConfirmationPane extends ContextualPaneBase { export default class DeleteCollectionConfirmationPane extends ContextualPaneBase {

View File

@@ -8,7 +8,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import DeleteDatabaseConfirmationPane from "./DeleteDatabaseConfirmationPane"; import DeleteDatabaseConfirmationPane from "./DeleteDatabaseConfirmationPane";
import DeleteFeedback from "../../Common/DeleteFeedback"; import DeleteFeedback from "../../Common/DeleteFeedback";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { TreeNode } from "../../Contracts/ViewModels"; import { TreeNode } from "../../Contracts/ViewModels";
import { TabsManager } from "../Tabs/TabsManager"; import { TabsManager } from "../Tabs/TabsManager";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
@@ -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

@@ -11,7 +11,7 @@ import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"
import DeleteFeedback from "../../Common/DeleteFeedback"; import DeleteFeedback from "../../Common/DeleteFeedback";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase { export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {

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

@@ -5,7 +5,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient"; import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { JunoUtils } from "../../Utils/JunoUtils"; import { JunoUtils } from "../../Utils/JunoUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";

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

@@ -6,7 +6,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab"; import QueryTab from "../Tabs/QueryTab";
export class SaveQueryPane extends ContextualPaneBase { export class SaveQueryPane extends ContextualPaneBase {

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

@@ -4,7 +4,7 @@ import { Areas, KeyCodes } from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as ko from "knockout"; import * as ko from "knockout";
export class SetupNotebooksPane extends ContextualPaneBase { export class SetupNotebooksPane extends ContextualPaneBase {

View File

@@ -5,7 +5,7 @@ import { DirectoryListProps } from "../Controls/Directory/DirectoryListComponent
import { DefaultDirectoryDropdownProps } from "../Controls/Directory/DefaultDirectoryDropdownComponent"; import { DefaultDirectoryDropdownProps } from "../Controls/Directory/DefaultDirectoryDropdownComponent";
import { DirectoryComponentAdapter } from "../Controls/Directory/DirectoryComponentAdapter"; import { DirectoryComponentAdapter } from "../Controls/Directory/DirectoryComponentAdapter";
import SwitchDirectoryPaneTemplate from "./SwitchDirectoryPane.html"; import SwitchDirectoryPaneTemplate from "./SwitchDirectoryPane.html";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
class PaneComponent { class PaneComponent {
constructor(data: any) { constructor(data: any) {

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

@@ -7,7 +7,7 @@ import * as CommonConstants from "../../../Common/Constants";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
// This is the format of the data we will have to pass to Datatable render callback, // This is the format of the data we will have to pass to Datatable render callback,

View File

@@ -15,7 +15,7 @@ import * as Utilities from "../Utilities";
import * as Entities from "../Entities"; import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as TableEntityProcessor from "../TableEntityProcessor"; import * as TableEntityProcessor from "../TableEntityProcessor";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as ErrorParserUtility from "../../../Common/ErrorParserUtility"; import * as ErrorParserUtility from "../../../Common/ErrorParserUtility";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";

View File

@@ -13,7 +13,7 @@ import * as HeadersUtility from "../../Common/HeadersUtility";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
import { DocumentsGridMetrics } from "../../Common/Constants"; import { DocumentsGridMetrics } from "../../Common/Constants";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";

View File

@@ -11,7 +11,7 @@ import editable from "../../Common/EditableUtility";
import Q from "q"; import Q from "q";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; 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";
@@ -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

@@ -14,7 +14,7 @@ import TabsBase from "./TabsBase";
import { DocumentsGridMetrics } from "../../Common/Constants"; import { DocumentsGridMetrics } from "../../Common/Constants";
import { QueryUtils } from "../../Utils/QueryUtils"; import { QueryUtils } from "../../Utils/QueryUtils";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import NewDocumentIcon from "../../../images/NewDocument.svg"; import NewDocumentIcon from "../../../images/NewDocument.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";

View File

@@ -8,7 +8,7 @@ import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import MongoUtility from "../../Common/MongoUtility"; import MongoUtility from "../../Common/MongoUtility";
import ObjectId from "../Tree/ObjectId"; import ObjectId from "../Tree/ObjectId";
import Q from "q"; import Q from "q";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { import {
createDocument, createDocument,

View File

@@ -6,7 +6,7 @@ import EnvironmentUtility from "../../Common/EnvironmentUtility";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import Q from "q"; import Q from "q";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
import * as 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 { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../Common/HashMap"; import { HashMap } from "../../Common/HashMap";

View File

@@ -17,7 +17,7 @@ import SaveIcon from "../../../images/save-cosmos.svg";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg"; import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg"; import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import * as 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 { Areas, ArmApiVersions } from "../../Common/Constants"; import { Areas, ArmApiVersions } from "../../Common/Constants";
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
@@ -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,17 +156,34 @@ 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", {
onCommandClick: () => this.notebookComponentAdapter.notebookSave(), iconName: "Save",
commandButtonLabel: saveLabel, onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
hasPopup: false, commandButtonLabel: saveLabel,
disabled: false, hasPopup: false,
ariaLabel: saveLabel disabled: false,
}, 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

@@ -10,7 +10,7 @@ import { HashMap } from "../../Common/HashMap";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import { QueryUtils } from "../../Utils/QueryUtils"; import { QueryUtils } from "../../Utils/QueryUtils";
import SaveQueryIcon from "../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg";

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

@@ -12,16 +12,17 @@ import editable from "../../Common/EditableUtility";
import Q from "q"; import Q from "q";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; 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,60 +1026,50 @@ 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()) { case "on":
case "on": defaultTtl = Number(this.timeToLiveSeconds());
defaultTtl = Number(this.timeToLiveSeconds()); break;
break; case "on-nodefault":
case "on-nodefault": defaultTtl = -1;
defaultTtl = -1; break;
break; case "off":
case "off": default:
default: defaultTtl = undefined;
defaultTtl = undefined; break;
break; }
}
newCollectionAttributes.defaultTtl = defaultTtl; newCollectionAttributes.defaultTtl = defaultTtl;
newCollectionAttributes.indexingPolicy = this.indexingPolicyContent(); newCollectionAttributes.indexingPolicy = this.indexingPolicyContent();
newCollectionAttributes.changeFeedPolicy = newCollectionAttributes.changeFeedPolicy =
this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On
? ({ ? ({
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
} as DataModels.ChangeFeedPolicy) } as DataModels.ChangeFeedPolicy)
: undefined;
newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
? this.analyticalStorageTtlSelection() === "on"
? Number(this.analyticalStorageTtlSeconds())
: Constants.AnalyticalStorageTtl.Infinite
: undefined; : undefined;
newCollectionAttributes.geospatialConfig = { newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
type: this.geospatialConfigType() ? this.analyticalStorageTtlSelection() === "on"
}; ? Number(this.analyticalStorageTtlSeconds())
: Constants.AnalyticalStorageTtl.Infinite
: undefined;
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy(); newCollectionAttributes.geospatialConfig = {
if (!!conflictResolutionChanges) { type: this.geospatialConfigType()
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges; };
}
const newCollection: DataModels.Collection = _.extend( const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
{}, if (!!conflictResolutionChanges) {
this.collection.rawDataModel, newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
newCollectionAttributes }
);
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newCollection
);
if (updatedCollection) { const newCollection: DataModels.Collection = _.extend({}, this.collection.rawDataModel, newCollectionAttributes);
const updateCollectionPromise = updateCollection(this.collection.databaseId, this.collection, newCollection).then(
(updatedCollection: DataModels.Collection) => {
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,133 +1079,164 @@ 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 (
this.throughput.editableIsDirty() ||
this.rupm.editableIsDirty() ||
this._isAutoPilotDirty() ||
this._hasProvisioningTypeChanged()
) {
const newThroughput = this.throughput();
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
if (newOffer.content) {
newOffer.content.offerThroughput = newThroughput;
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
} else {
newOffer = _.extend({}, newOffer, {
content: {
offerThroughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
}
});
}
const headerOptions: RequestOptions = { initialHeaders: {} };
if (this.isAutoPilotSelected()) {
if (!this.hasAutoPilotV2FeatureFlag()) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: this.autoPilotThroughput()
};
} else {
newOffer.content.offerAutopilotSettings = {
tier: this.selectedAutoPilotTier()
};
}
// user has changed from provisioned --> autoscale
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
delete newOffer.content.offerAutopilotSettings;
} else {
delete newOffer.content.offerThroughput;
}
} else {
this.isAutoPilotSelected(false);
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
// user has changed from autoscale --> provisioned
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
} else {
delete newOffer.content.offerAutopilotSettings;
}
} }
if ( if (
this.throughput.editableIsDirty() || this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.rupm.editableIsDirty() || newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this._isAutoPilotDirty() || this.container != null
this._hasProvisioningTypeChanged()
) { ) {
const newThroughput = this.throughput(); const requestPayload = {
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on; subscriptionId: userContext.subscriptionId,
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer()); databaseAccountName: userContext.databaseAccount.name,
const originalThroughputValue: number = this.throughput.getEditableOriginalValue(); resourceGroup: userContext.resourceGroup,
databaseName: this.collection.databaseId,
if (newOffer.content) { collectionName: this.collection.id(),
newOffer.content.offerThroughput = newThroughput; throughput: newThroughput,
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled; offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
} else { };
newOffer = _.extend({}, newOffer, { const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
content: { () => {
offerThroughput: newThroughput, this.collection.offer().content.offerThroughput = originalThroughputValue;
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled this.throughput(originalThroughputValue);
} this.notificationStatusInfo(
}); throughputApplyDelayedMessage(
} this.isAutoPilotSelected(),
originalThroughputValue,
const headerOptions: RequestOptions = { initialHeaders: {} }; this._getThroughputUnit(),
this.collection.databaseId,
if (this.isAutoPilotSelected()) { this.collection.id(),
if (!this.hasAutoPilotV2FeatureFlag()) { newThroughput
newOffer.content.offerAutopilotSettings = { )
maxThroughput: this.autoPilotThroughput() );
}; this.throughput.valueHasMutated(); // force component re-render
} else { },
newOffer.content.offerAutopilotSettings = { (error: any) => {
tier: this.selectedAutoPilotTier() TelemetryProcessor.traceFailure(
}; Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: error
},
startKey
);
} }
);
// user has changed from provisioned --> autoscale promises.push(Q(updateOfferBeyondLimitPromise));
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) { } else {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true"; const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
delete newOffer.content.offerAutopilotSettings; (updatedOffer: DataModels.Offer) => {
} else { this.collection.offer(updatedOffer);
delete newOffer.content.offerThroughput; this.collection.offer.valueHasMutated();
} }
} else { );
this.isAutoPilotSelected(false);
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
// user has changed from autoscale --> provisioned promises.push(updateOfferPromise);
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
} else {
delete newOffer.content.offerAutopilotSettings;
}
}
if (
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.container != null
) {
const requestPayload = {
subscriptionId: userContext.subscriptionId,
databaseAccountName: userContext.databaseAccount.name,
resourceGroup: userContext.resourceGroup,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
throughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
};
await updateOfferThroughputBeyondLimit(requestPayload);
this.collection.offer().content.offerThroughput = originalThroughputValue;
this.throughput(originalThroughputValue);
this.notificationStatusInfo(
throughputApplyDelayedMessage(
this.isAutoPilotSelected(),
originalThroughputValue,
this._getThroughputUnit(),
this.collection.databaseId,
this.collection.id(),
newThroughput
)
);
this.throughput.valueHasMutated(); // force component re-render
} else {
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
this.collection.offer(updatedOffer);
this.collection.offer.valueHasMutated();
}
} }
this.container.isRefreshingExplorer(false);
this._setBaseline();
this.collection.readSettings();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
TelemetryProcessor.traceSuccess(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
startKey
);
} catch (error) {
this.container.isRefreshingExplorer(false);
this.isExecutionError(true);
console.error(error);
TelemetryProcessor.traceFailure(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: error
},
startKey
);
} }
this.isExecuting(false); if (promises.length === 0) {
this.isExecuting(false);
}
return Q.all(promises)
.then(
() => {
this.container.isRefreshingExplorer(false);
this._setBaseline();
this.collection.readSettings();
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
TelemetryProcessor.traceSuccess(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
startKey
);
},
(reason: any) => {
this.container.isRefreshingExplorer(false);
this.isExecutionError(true);
console.error(reason);
TelemetryProcessor.traceFailure(
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
}; };
public onRevertClick = (): Q.Promise<any> => { public onRevertClick = (): Q.Promise<any> => {

View File

@@ -7,7 +7,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import editable from "../../Common/EditableUtility"; import editable from "../../Common/EditableUtility";
import ScriptTabBase from "./ScriptTabBase"; import ScriptTabBase from "./ScriptTabBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import StoredProcedure from "../Tree/StoredProcedure"; import StoredProcedure from "../Tree/StoredProcedure";
import { createStoredProcedure, updateStoredProcedure } from "../../Common/DocumentClientUtilityBase"; import { createStoredProcedure, updateStoredProcedure } from "../../Common/DocumentClientUtilityBase";

View File

@@ -5,7 +5,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { RouteHandler } from "../../RouteHandlers/RouteHandler"; import { RouteHandler } from "../../RouteHandlers/RouteHandler";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ThemeUtility from "../../Common/ThemeUtility"; import ThemeUtility from "../../Common/ThemeUtility";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";

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

@@ -5,7 +5,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import ScriptTabBase from "./ScriptTabBase"; import ScriptTabBase from "./ScriptTabBase";
import editable from "../../Common/EditableUtility"; import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Trigger from "../Tree/Trigger"; import Trigger from "../Tree/Trigger";
import { createTrigger, updateTrigger } from "../../Common/DocumentClientUtilityBase"; import { createTrigger, updateTrigger } from "../../Common/DocumentClientUtilityBase";

View File

@@ -4,7 +4,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import ScriptTabBase from "./ScriptTabBase"; import ScriptTabBase from "./ScriptTabBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import UserDefinedFunction from "../Tree/UserDefinedFunction"; import UserDefinedFunction from "../Tree/UserDefinedFunction";
import { createUserDefinedFunction, updateUserDefinedFunction } from "../../Common/DocumentClientUtilityBase"; import { createUserDefinedFunction, updateUserDefinedFunction } from "../../Common/DocumentClientUtilityBase";

View File

@@ -9,7 +9,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { PlatformType } from "../../PlatformType"; import { PlatformType } from "../../PlatformType";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { OfferUtils } from "../../Utils/OfferUtils"; import { OfferUtils } from "../../Utils/OfferUtils";
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions"; import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
@@ -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( this.container.tabsManager.activateTab(settingsTab);
(pendingNotification: DataModels.Notification) => {
settingsTab.pendingNotification(pendingNotification);
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

@@ -7,7 +7,7 @@ import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab"; import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
import Collection from "./Collection"; import Collection from "./Collection";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
@@ -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( this.container.tabsManager.activateTab(settingsTab);
(pendingNotification: DataModels.Notification) => {
settingsTab.pendingNotification(pendingNotification);
this.container.tabsManager.activateTab(settingsTab);
},
(error: any) => {
settingsTab.pendingNotification(undefined);
this.container.tabsManager.activateTab(settingsTab);
}
);
} }
}; };
public readSettings(): Q.Promise<void> { 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

@@ -7,7 +7,7 @@ import DocumentId from "./DocumentId";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import Q from "q"; import Q from "q";
import QueryTab from "../Tabs/QueryTab"; import QueryTab from "../Tabs/QueryTab";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";

View File

@@ -1,112 +0,0 @@
import Explorer from "../Explorer";
import * as ko from "knockout";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "../Tabs/TabsBase";
describe("ResourceTreeAdapter", () => {
const mockContainer = (): Explorer =>
(({
selectedNode: ko.observable<ViewModels.TreeNode>({
nodeKind: "nodeKind",
rid: "rid",
id: ko.observable<string>("id")
}),
tabsManager: {
activeTab: ko.observable<TabsBase>({
tabKind: ViewModels.CollectionTabKind.Documents
} as TabsBase)
},
isNotebookEnabled: ko.observable<boolean>(true),
nonSystemDatabases: ko.observable<ViewModels.Database[]>([])
} as unknown) as Explorer);
// TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths
describe("isDataNodeSelected", () => {
it("it should not select if no selected node", () => {
const explorer = mockContainer();
explorer.selectedNode(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy();
});
it("it should not select incorrect subnodekinds", () => {
const resourceTreeAdapter = new ResourceTreeAdapter(mockContainer());
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy();
});
it("it should not select if no active tab", () => {
const explorer = mockContainer();
explorer.tabsManager.activeTab(undefined);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("foo", "bar", undefined);
expect(isDataNodeSelected).toBeFalsy();
});
it("should select if correct database node regardless of subnodekinds", () => {
const subNodeKind = ViewModels.CollectionTabKind.Documents;
const explorer = mockContainer();
explorer.selectedNode(({
nodeKind: "Database",
rid: "dbrid",
id: ko.observable<string>("id"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind)
} as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("dbrid", "Database", [
ViewModels.CollectionTabKind.Documents
]);
expect(isDataNodeSelected).toBeTruthy();
});
it("should select correct collection node (documents or graph node)", () => {
let subNodeKind = ViewModels.CollectionTabKind.Documents;
const explorer = mockContainer();
explorer.tabsManager.activeTab({
tabKind: subNodeKind
} as TabsBase);
explorer.selectedNode(({
nodeKind: "Collection",
rid: "collrid",
id: ko.observable<string>("id"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind)
} as unknown) as ViewModels.TreeNode);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
let isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("collrid", "Collection", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy();
subNodeKind = ViewModels.CollectionTabKind.Graph;
explorer.tabsManager.activeTab({
tabKind: subNodeKind
} as TabsBase);
explorer.selectedNode(({
nodeKind: "Collection",
rid: "collrid",
id: ko.observable<string>("id"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(subNodeKind)
} as unknown) as ViewModels.TreeNode);
isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("collrid", "Collection", [subNodeKind]);
expect(isDataNodeSelected).toBeTruthy();
});
it("should not select incorrect collection node (e.g. Settings)", () => {
const explorer = mockContainer();
explorer.selectedNode(({
nodeKind: "Collection",
rid: "collrid",
id: ko.observable<string>("id"),
selectedSubnodeKind: ko.observable<ViewModels.CollectionTabKind>(ViewModels.CollectionTabKind.Documents)
} as unknown) as ViewModels.TreeNode);
explorer.tabsManager.activeTab({
tabKind: ViewModels.CollectionTabKind.Documents
} as TabsBase);
const resourceTreeAdapter = new ResourceTreeAdapter(explorer);
const isDataNodeSelected = resourceTreeAdapter.isDataNodeSelected("collrid", "Collection", [
ViewModels.CollectionTabKind.Settings
]);
expect(isDataNodeSelected).toBeFalsy();
});
});
});

View File

@@ -19,7 +19,7 @@ import { ArrayHashMap } from "../../Common/ArrayHashMap";
import { NotebookUtil } from "../Notebook/NotebookUtil"; import { NotebookUtil } from "../Notebook/NotebookUtil";
import _ from "underscore"; import _ from "underscore";
import { IPinnedRepo } from "../../Juno/JunoClient"; import { IPinnedRepo } from "../../Juno/JunoClient";
import * as 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 { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
@@ -191,7 +191,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
databaseNode.children.push({ databaseNode.children.push({
label: "Scale", label: "Scale",
isSelected: () => isSelected: () =>
this.isDataNodeSelected(database.rid, "Database", [ViewModels.CollectionTabKind.DatabaseSettings]), this.isDataNodeSelected(database.rid, "Database", ViewModels.CollectionTabKind.DatabaseSettings),
onClick: database.onSettingsClick.bind(database) onClick: database.onSettingsClick.bind(database)
}); });
} }
@@ -235,18 +235,14 @@ export class ResourceTreeAdapter implements ReactAdapter {
data: collection.rid data: collection.rid
}); });
}, },
isSelected: () => isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Documents),
this.isDataNodeSelected(collection.rid, "Collection", [
ViewModels.CollectionTabKind.Documents,
ViewModels.CollectionTabKind.Graph
]),
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection) contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection)
}); });
children.push({ children.push({
label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings",
onClick: collection.onSettingsClick.bind(collection), onClick: collection.onSettingsClick.bind(collection),
isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", [ViewModels.CollectionTabKind.Settings]) isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Settings)
}); });
if (ResourceTreeAdapter.showScriptNodes(this.container)) { if (ResourceTreeAdapter.showScriptNodes(this.container)) {
@@ -268,8 +264,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
children.push({ children.push({
label: "Conflicts", label: "Conflicts",
onClick: collection.onConflictsClick.bind(collection), onClick: collection.onConflictsClick.bind(collection),
isSelected: () => isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Conflicts)
this.isDataNodeSelected(collection.rid, "Collection", [ViewModels.CollectionTabKind.Conflicts])
}); });
} }
@@ -307,7 +302,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: sp.id(), label: sp.id(),
onClick: sp.open.bind(sp), onClick: sp.open.bind(sp),
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.rid, "Collection", [ViewModels.CollectionTabKind.StoredProcedures]), this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.StoredProcedures),
contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp) contextMenu: ResourceTreeContextMenuButtonFactory.createStoreProcedureContextMenuItems(this.container, sp)
})), })),
onClick: () => { onClick: () => {
@@ -326,7 +321,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
label: udf.id(), label: udf.id(),
onClick: udf.open.bind(udf), onClick: udf.open.bind(udf),
isSelected: () => isSelected: () =>
this.isDataNodeSelected(collection.rid, "Collection", [ViewModels.CollectionTabKind.UserDefinedFunctions]), this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.UserDefinedFunctions),
contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container, udf) contextMenu: ResourceTreeContextMenuButtonFactory.createUserDefinedFunctionContextMenuItems(this.container, udf)
})), })),
onClick: () => { onClick: () => {
@@ -344,8 +339,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
children: collection.triggers().map((trigger: Trigger) => ({ children: collection.triggers().map((trigger: Trigger) => ({
label: trigger.id(), label: trigger.id(),
onClick: trigger.open.bind(trigger), onClick: trigger.open.bind(trigger),
isSelected: () => isSelected: () => this.isDataNodeSelected(collection.rid, "Collection", ViewModels.CollectionTabKind.Triggers),
this.isDataNodeSelected(collection.rid, "Collection", [ViewModels.CollectionTabKind.Triggers]),
contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger) contextMenu: ResourceTreeContextMenuButtonFactory.createTriggerContextMenuItems(this.container, trigger)
})), })),
onClick: () => { onClick: () => {
@@ -552,52 +546,43 @@ export class ResourceTreeAdapter implements ReactAdapter {
(activeTab as any).notebookPath() === item.path (activeTab as any).notebookPath() === item.path
); );
}, },
contextMenu: createFileContextMenu && this.createFileContextMenu(item), contextMenu: createFileContextMenu
? [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item)
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
this.container.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined
);
}
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => this.copyNotebook(item)
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => this.container.downloadFile(item)
}
]
: undefined,
data: item data: item
}; };
} }
private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
let items: TreeNodeMenuItem[] = [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item)
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
this.container.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined
);
}
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => this.copyNotebook(item)
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => this.container.downloadFile(item)
}
];
// "Copy to ..." isn't needed if github locations are not available
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter(item => item.label !== "Copy to ...");
}
return items;
}
private copyNotebook = async (item: NotebookContentItem) => { private copyNotebook = async (item: NotebookContentItem) => {
const content = await this.container.readFile(item); const content = await this.container.readFile(item);
if (content) { if (content) {
@@ -703,19 +688,13 @@ export class ResourceTreeAdapter implements ReactAdapter {
window.requestAnimationFrame(() => this.parameters(Date.now())); window.requestAnimationFrame(() => this.parameters(Date.now()));
} }
/** private isDataNodeSelected(rid: string, nodeKind: string, subnodeKind: ViewModels.CollectionTabKind): boolean {
* public for testing purposes
* @param rid
* @param nodeKind
* @param subnodeKinds
*/
public isDataNodeSelected(rid: string, nodeKind: string, subnodeKinds: ViewModels.CollectionTabKind[]): boolean {
if (!this.container.selectedNode || !this.container.selectedNode()) { if (!this.container.selectedNode || !this.container.selectedNode()) {
return false; return false;
} }
const selectedNode = this.container.selectedNode(); const selectedNode = this.container.selectedNode();
if (subnodeKinds === undefined || !Array.isArray(subnodeKinds)) { if (subnodeKind === undefined) {
return selectedNode.rid === rid && selectedNode.nodeKind === nodeKind; return selectedNode.rid === rid && selectedNode.nodeKind === nodeKind;
} else { } else {
const activeTab = this.container.tabsManager.activeTab(); const activeTab = this.container.tabsManager.activeTab();
@@ -728,10 +707,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
return ( return (
activeTab && activeTab &&
subnodeKinds.includes(activeTab.tabKind) && activeTab.tabKind === subnodeKind &&
selectedNode.rid === rid && selectedNode.rid === rid &&
selectedSubnodeKind !== undefined && selectedSubnodeKind !== undefined &&
subnodeKinds.includes(selectedSubnodeKind) selectedSubnodeKind === subnodeKind
); );
} }
} }

View File

@@ -5,7 +5,7 @@ import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import StoredProcedureTab from "../Tabs/StoredProcedureTab"; import StoredProcedureTab from "../Tabs/StoredProcedureTab";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { deleteStoredProcedure, executeStoredProcedure } from "../../Common/DocumentClientUtilityBase"; import { deleteStoredProcedure, executeStoredProcedure } from "../../Common/DocumentClientUtilityBase";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";

View File

@@ -4,7 +4,7 @@ import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import TriggerTab from "../Tabs/TriggerTab"; import TriggerTab from "../Tabs/TriggerTab";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { deleteTrigger } from "../../Common/DocumentClientUtilityBase"; import { deleteTrigger } from "../../Common/DocumentClientUtilityBase";

View File

@@ -4,7 +4,7 @@ import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { deleteUserDefinedFunction } from "../../Common/DocumentClientUtilityBase"; import { deleteUserDefinedFunction } from "../../Common/DocumentClientUtilityBase";

View File

@@ -29,7 +29,7 @@ import { MeControlComponentAdapter } from "./Explorer/Menus/NavBar/MeControlComp
import { MessageTypes } from "./Contracts/ExplorerContracts"; import { MessageTypes } from "./Contracts/ExplorerContracts";
import * as ReactBindingHandler from "./Bindings/ReactBindingHandler"; import * as ReactBindingHandler from "./Bindings/ReactBindingHandler";
import { SwitchDirectoryPane, SwitchDirectoryPaneComponent } from "./Explorer/Panes/SwitchDirectoryPane"; import { SwitchDirectoryPane, SwitchDirectoryPaneComponent } from "./Explorer/Panes/SwitchDirectoryPane";
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
import { isInvalidParentFrameOrigin } from "./Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "./Utils/MessageValidation";
import "../less/hostedexplorer.less"; import "../less/hostedexplorer.less";
import "./Explorer/Menus/NavBar/MeControlComponent.less"; import "./Explorer/Menus/NavBar/MeControlComponent.less";

View File

@@ -60,7 +60,7 @@ import "url-polyfill/url-polyfill.min";
// import "./ReactDevTools" // import "./ReactDevTools"
import * as ko from "knockout"; import * as ko from "knockout";
import * as TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "./Shared/Telemetry/TelemetryProcessor";
import * as ViewModels from "./Contracts/ViewModels"; import * as ViewModels from "./Contracts/ViewModels";
import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "./Shared/Telemetry/TelemetryConstants";

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 {

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