mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-05 18:47:41 +00:00
Compare commits
23 Commits
add-agcs-t
...
replace-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f895d343 | ||
|
|
92073a5646 | ||
|
|
3bc2701356 | ||
|
|
35dbaeea96 | ||
|
|
18745a9ae6 | ||
|
|
5c84b3a7d4 | ||
|
|
3223ff7685 | ||
|
|
38732af907 | ||
|
|
e837f574a8 | ||
|
|
47a5c315b5 | ||
|
|
1c80ced259 | ||
|
|
5e6ac78b7d | ||
|
|
999196193f | ||
|
|
951289e190 | ||
|
|
3279460cfd | ||
|
|
07b9c1d1b7 | ||
|
|
dde2ca75c4 | ||
|
|
f44a3da568 | ||
|
|
22b2e1df48 | ||
|
|
5be6f982f9 | ||
|
|
4fc9393b76 | ||
|
|
ee51e873b8 | ||
|
|
206a8ef93b |
@@ -41,6 +41,7 @@ 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"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -5,13 +5,14 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/cosmos": {
|
"@azure/cosmos": {
|
||||||
"version": "3.7.4",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.9.0.tgz",
|
||||||
"integrity": "sha512-IbSEadapQDajSCXj7gUc8OklkOd/oAY4w7XBLHouWc4iKQTtntb2DmGjhrbh2W5Ku+pmBSr1GTApCjQ55iIjlQ==",
|
"integrity": "sha512-SA+QB54I8Dvg/ZolHpsEDLK/sbSB9sFmSU1ElnMTFw88TVik+LYHq4o/srU2TY6Gr1BketjPmgLVEqrmnRvjkw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/debug": "^4.1.4",
|
"@types/debug": "^4.1.4",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"jsbi": "^3.1.3",
|
||||||
"node-abort-controller": "^1.0.4",
|
"node-abort-controller": "^1.0.4",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"os-name": "^3.1.0",
|
"os-name": "^3.1.0",
|
||||||
@@ -22,14 +23,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
|
||||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "8.2.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
||||||
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
|
"integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20204,6 +20205,11 @@
|
|||||||
"esprima": "^4.0.0"
|
"esprima": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jsbi": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-nBJqA0C6Qns+ZxurbEoIR56wyjiUszpNy70FHvxO5ervMoCbZVE3z3kxr5nKGhlxr/9MhKTSUBs7cAwwuf3g9w=="
|
||||||
|
},
|
||||||
"jsbn": {
|
"jsbn": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/cosmos": "3.7.4",
|
"@azure/cosmos": "3.9.0",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ 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 {
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ 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,
|
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 { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
||||||
import { client } from "./CosmosClient";
|
import { client } from "./CosmosClient";
|
||||||
@@ -202,23 +200,6 @@ 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,
|
||||||
@@ -244,7 +225,8 @@ export function updateOffer(
|
|||||||
return Q(
|
return Q(
|
||||||
client()
|
client()
|
||||||
.offer(offer.id)
|
.offer(offer.id)
|
||||||
.replace(newOffer, options)
|
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||||
|
.replace((newOffer as unknown) as OfferDefinition, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
||||||
})
|
})
|
||||||
@@ -454,6 +436,10 @@ 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, [
|
||||||
@@ -469,6 +455,13 @@ 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;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,26 +543,6 @@ export function getOrCreateDatabaseAndCollection(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDatabase(
|
|
||||||
request: DataModels.CreateDatabaseRequest,
|
|
||||||
options: any
|
|
||||||
): Q.Promise<DataModels.Database> {
|
|
||||||
var deferred = Q.defer<DataModels.Database>();
|
|
||||||
|
|
||||||
_createDatabase(request, options).then(
|
|
||||||
(createdDatabase: DataModels.Database) => {
|
|
||||||
refreshCachedOffers().then(() => {
|
|
||||||
deferred.resolve(createdDatabase);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
_createDatabaseError => {
|
|
||||||
deferred.reject(_createDatabaseError);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function refreshCachedOffers(): Q.Promise<void> {
|
export function refreshCachedOffers(): Q.Promise<void> {
|
||||||
if (configContext.platform === Platform.Portal) {
|
if (configContext.platform === Platform.Portal) {
|
||||||
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
|
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
|
||||||
@@ -598,33 +571,3 @@ 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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -266,42 +266,6 @@ 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,
|
||||||
@@ -926,36 +890,3 @@ export function getOrCreateDatabaseAndCollection(
|
|||||||
|
|
||||||
return deferred.promise;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
250
src/Common/dataAccess/createDatabase.ts
Normal file
250
src/Common/dataAccess/createDatabase.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseResponse } from "@azure/cosmos";
|
||||||
|
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import {
|
||||||
|
SqlDatabaseCreateUpdateParameters,
|
||||||
|
CreateUpdateOptions
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { createUpdateSqlDatabase, getSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import {
|
||||||
|
createUpdateCassandraKeyspace,
|
||||||
|
getCassandraKeyspace
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import {
|
||||||
|
createUpdateMongoDBDatabase,
|
||||||
|
getMongoDBDatabase
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import {
|
||||||
|
createUpdateGremlinDatabase,
|
||||||
|
getGremlinDatabase
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
let database: DataModels.Database;
|
||||||
|
const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
!userContext.useSDKOperations &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
|
) {
|
||||||
|
database = await createDatabaseWithARM(params);
|
||||||
|
} else {
|
||||||
|
database = await createDatabaseWithSDK(params);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while creating database ${params.databaseId}:\n ${error.message}`);
|
||||||
|
logError(JSON.stringify(error), "CreateDatabase", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
clearMessage();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logConsoleInfo(`Successfully created database ${params.databaseId}`);
|
||||||
|
await refreshCachedResources();
|
||||||
|
await refreshCachedOffers();
|
||||||
|
clearMessage();
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
return createSqlDatabase(params);
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
return createMongoDatabase(params);
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
return createCassandraKeyspace(params);
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
return createGremlineDatabase(params);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getSqlDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateSqlDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getMongoDBDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateMongoDBDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getCassandraKeyspace(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse?.properties?.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateCassandraKeyspace(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGremlineDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
try {
|
||||||
|
const getResponse = await getGremlinDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId
|
||||||
|
);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||||
|
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||||
|
properties: {
|
||||||
|
resource: {
|
||||||
|
id: params.databaseId
|
||||||
|
},
|
||||||
|
options
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createResponse = await createUpdateGremlinDatabase(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
params.databaseId,
|
||||||
|
rpPayload
|
||||||
|
);
|
||||||
|
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||||
|
const createBody: DatabaseRequest = { id: params.databaseId };
|
||||||
|
const databaseOptions: RequestOptions = {};
|
||||||
|
// TODO: replace when SDK support autopilot
|
||||||
|
if (params.databaseLevelThroughput) {
|
||||||
|
if (params.autoPilotMaxThroughput) {
|
||||||
|
createBody.maxThroughput = params.autoPilotMaxThroughput;
|
||||||
|
} else {
|
||||||
|
createBody.throughput = params.offerThroughput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: DatabaseResponse = await client().databases.create(createBody, databaseOptions);
|
||||||
|
return response.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructRpOptions(params: DataModels.CreateDatabaseParams): CreateUpdateOptions {
|
||||||
|
if (!params.databaseLevelThroughput) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.autoPilotMaxThroughput) {
|
||||||
|
return {
|
||||||
|
autoscaleSettings: {
|
||||||
|
maxThroughput: params.autoPilotMaxThroughput
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
throughput: params.offerThroughput
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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) {
|
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||||
await deleteCollectionWithARM(databaseId, collectionId);
|
await deleteCollectionWithARM(databaseId, collectionId);
|
||||||
} else {
|
} else {
|
||||||
await client()
|
await client()
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
|||||||
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
|
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.authType === AuthType.AAD) {
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
|
||||||
|
!userContext.useSDKOperations
|
||||||
|
) {
|
||||||
await deleteDatabaseWithARM(databaseId);
|
await deleteDatabaseWithARM(databaseId);
|
||||||
} else {
|
} else {
|
||||||
await client()
|
await client()
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ 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 (window.authType === AuthType.AAD) {
|
if (
|
||||||
|
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()
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ 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 (window.authType === AuthType.AAD) {
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
!userContext.useSDKOperations &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
|
||||||
|
) {
|
||||||
databases = await readDatabasesWithARM();
|
databases = await readDatabasesWithARM();
|
||||||
} else {
|
} else {
|
||||||
const sdkResponse = await client()
|
const sdkResponse = await client()
|
||||||
|
|||||||
225
src/Common/dataAccess/updateCollection.ts
Normal file
225
src/Common/dataAccess/updateCollection.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { Collection } from "../../Contracts/DataModels";
|
||||||
|
import { ContainerDefinition } from "@azure/cosmos";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import {
|
||||||
|
ExtendedResourceProperties,
|
||||||
|
SqlContainerCreateUpdateParameters,
|
||||||
|
SqlContainerResource
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import {
|
||||||
|
createUpdateCassandraTable,
|
||||||
|
getCassandraTable
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import {
|
||||||
|
createUpdateMongoDBCollection,
|
||||||
|
getMongoDBCollection
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import {
|
||||||
|
createUpdateGremlinGraph,
|
||||||
|
getGremlinGraph
|
||||||
|
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function updateCollection(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
newCollection: Collection,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<Collection> {
|
||||||
|
let collection: Collection;
|
||||||
|
const clearMessage = logConsoleProgress(`Updating container ${collectionId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
window.authType === AuthType.AAD &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
|
||||||
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
|
) {
|
||||||
|
collection = await updateCollectionWithARM(databaseId, collectionId, newCollection);
|
||||||
|
} else {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.replace(newCollection as ContainerDefinition, options);
|
||||||
|
collection = sdkResponse.resource as Collection;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "UpdateCollection", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logConsoleInfo(`Successfully updated container ${collectionId}`);
|
||||||
|
clearMessage();
|
||||||
|
await refreshCachedResources();
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCollectionWithARM(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
return updateSqlContainer(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
return updateMongoDBCollection(
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
newCollection
|
||||||
|
);
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
return updateCassandraTable(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
return updateGremlinGraph(databaseId, collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
case DefaultAccountExperienceType.Table:
|
||||||
|
return updateTable(collectionId, subscriptionId, resourceGroup, accountName, newCollection);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSqlContainer(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateSqlContainer(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Sql container to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateMongoDBCollection(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateMongoDBCollection(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`MongoDB collection to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCassandraTable(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateCassandraTable(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Cassandra table to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateGremlinGraph(
|
||||||
|
databaseId: string,
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateGremlinGraph(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
databaseId,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Gremlin graph to update does not exist. Database id: ${databaseId} Collection id: ${collectionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTable(
|
||||||
|
collectionId: string,
|
||||||
|
subscriptionId: string,
|
||||||
|
resourceGroup: string,
|
||||||
|
accountName: string,
|
||||||
|
newCollection: Collection
|
||||||
|
): Promise<Collection> {
|
||||||
|
const getResponse = await getTable(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
|
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||||
|
getResponse.properties.resource = newCollection as SqlContainerResource & ExtendedResourceProperties;
|
||||||
|
const updateResponse = await createUpdateTable(
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
accountName,
|
||||||
|
collectionId,
|
||||||
|
getResponse as SqlContainerCreateUpdateParameters
|
||||||
|
);
|
||||||
|
return updateResponse && (updateResponse.properties.resource as Collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Table to update does not exist. Table id: ${collectionId}`);
|
||||||
|
}
|
||||||
@@ -80,12 +80,20 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Allow override of any config value with URL query parameters
|
// Allow override of platform value with URL query parameter
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
params.forEach((value, key) => {
|
if (params.has("platform")) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const platform = params.get("platform");
|
||||||
(configContext as any)[key] = value;
|
switch (platform) {
|
||||||
});
|
default:
|
||||||
|
console.log("Invalid platform query parameter given, ignoring");
|
||||||
|
break;
|
||||||
|
case Platform.Portal:
|
||||||
|
case Platform.Hosted:
|
||||||
|
case Platform.Emulator:
|
||||||
|
updateConfigContext({ platform });
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("No configuration file found using defaults");
|
console.log("No configuration file found using defaults");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,14 @@ 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[];
|
||||||
@@ -320,12 +327,11 @@ export interface AutoPilotOfferSettings {
|
|||||||
targetMaxThroughput?: number;
|
targetMaxThroughput?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateDatabaseRequest {
|
export interface CreateDatabaseParams {
|
||||||
|
autoPilotMaxThroughput?: number;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseLevelThroughput?: boolean;
|
databaseLevelThroughput?: boolean;
|
||||||
offerThroughput?: number;
|
offerThroughput?: number;
|
||||||
autoPilot?: AutoPilotCreationSettings;
|
|
||||||
hasAutoPilotV2FeatureFlag?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharedThroughputRange {
|
export interface SharedThroughputRange {
|
||||||
|
|||||||
@@ -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 } from "../ConfigContext";
|
import { configContext, updateConfigContext } 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";
|
||||||
@@ -975,6 +975,10 @@ export default class Explorer {
|
|||||||
this.sparkClusterConnectionInfo.valueHasMutated();
|
this.sparkClusterConnectionInfo.valueHasMutated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
|
||||||
|
updateUserContext({ useSDKOperations: true });
|
||||||
|
}
|
||||||
|
|
||||||
featureSubcription.dispose();
|
featureSubcription.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1475,38 +1479,33 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isServerlessEnabled()) {
|
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
|
||||||
// Serverless accounts don't support offers call
|
this._setLoadingStatusText("Fetching offers...");
|
||||||
refreshDatabases();
|
offerPromise.then(
|
||||||
} else {
|
(offers: DataModels.Offer[]) => {
|
||||||
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers();
|
this._setLoadingStatusText("Successfully fetched offers.");
|
||||||
this._setLoadingStatusText("Fetching offers...");
|
refreshDatabases(offers);
|
||||||
offerPromise.then(
|
},
|
||||||
(offers: DataModels.Offer[]) => {
|
error => {
|
||||||
this._setLoadingStatusText("Successfully fetched offers.");
|
this._setLoadingStatusText("Failed to fetch offers.");
|
||||||
refreshDatabases(offers);
|
this.isRefreshingExplorer(false);
|
||||||
},
|
deferred.reject(error);
|
||||||
error => {
|
TelemetryProcessor.traceFailure(
|
||||||
this._setLoadingStatusText("Failed to fetch offers.");
|
Action.LoadDatabases,
|
||||||
this.isRefreshingExplorer(false);
|
{
|
||||||
deferred.reject(error);
|
databaseAccountName: this.databaseAccount().name,
|
||||||
TelemetryProcessor.traceFailure(
|
defaultExperience: this.defaultExperience(),
|
||||||
Action.LoadDatabases,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
{
|
error: JSON.stringify(error)
|
||||||
databaseAccountName: this.databaseAccount().name,
|
},
|
||||||
defaultExperience: this.defaultExperience(),
|
startKey
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
);
|
||||||
error: JSON.stringify(error)
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
},
|
ConsoleDataType.Error,
|
||||||
startKey
|
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||||
);
|
);
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
}
|
||||||
ConsoleDataType.Error,
|
);
|
||||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deferred.promise.then(
|
return deferred.promise.then(
|
||||||
() => {
|
() => {
|
||||||
@@ -1954,12 +1953,17 @@ 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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -92,6 +92,11 @@ describe("getPkIdFromDocumentId", () => {
|
|||||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[234, 'id']");
|
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']");
|
||||||
|
|||||||
@@ -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") {
|
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
|
||||||
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"];
|
||||||
|
|||||||
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
import { Channels } from "@nteract/messaging";
|
||||||
|
import * as monaco from "./monaco";
|
||||||
|
import * as React from "react";
|
||||||
|
import { completionProvider } from "./completions/completionItemProvider";
|
||||||
|
import { AppState, ContentRef } from "@nteract/core";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import "./styles.css";
|
||||||
|
import { LightThemeName, HCLightThemeName, DarkThemeName } from "./theme";
|
||||||
|
// import { logger } from "src/common/localLogger";
|
||||||
|
import { getCellMonacoLanguage } from "./selectors";
|
||||||
|
// import { DocumentUri } from "./documentUri";
|
||||||
|
|
||||||
|
export type IModelContentChangedEvent = monaco.editor.IModelContentChangedEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial props for Monaco received from agnostic component
|
||||||
|
*/
|
||||||
|
export interface IMonacoProps {
|
||||||
|
id: string;
|
||||||
|
contentRef: ContentRef;
|
||||||
|
modelUri?: monaco.Uri;
|
||||||
|
theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string;
|
||||||
|
cellLanguageOverride?: string;
|
||||||
|
notebookLanguageOverride?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
channels: Channels | undefined;
|
||||||
|
enableCompletion: boolean;
|
||||||
|
shouldRegisterDefaultCompletion?: boolean;
|
||||||
|
onChange: (value: string, event?: unknown) => void;
|
||||||
|
onFocusChange: (focus: boolean) => void;
|
||||||
|
onCursorPositionChange?: (selection: monaco.ISelection | null) => void;
|
||||||
|
onRegisterCompletionProvider?: (languageId: string) => void;
|
||||||
|
value: string;
|
||||||
|
editorFocused: boolean;
|
||||||
|
lineNumbers: boolean;
|
||||||
|
|
||||||
|
/** set height of editor to fit the specified number of lines in display */
|
||||||
|
numberOfLines?: number;
|
||||||
|
|
||||||
|
options?: monaco.editor.IEditorOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monaco specific props derived from State
|
||||||
|
*/
|
||||||
|
interface IMonacoStateProps {
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the custom theme data to avoid repeatly defining the custom theme
|
||||||
|
let customThemeData: monaco.editor.IStandaloneThemeData;
|
||||||
|
|
||||||
|
function getMonacoTheme(theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string) {
|
||||||
|
if (typeof theme === "string") {
|
||||||
|
switch (theme) {
|
||||||
|
case "vs-dark":
|
||||||
|
return DarkThemeName;
|
||||||
|
case "hc-black":
|
||||||
|
return "hc-black";
|
||||||
|
case "vs":
|
||||||
|
return LightThemeName;
|
||||||
|
case "hc-light":
|
||||||
|
return HCLightThemeName;
|
||||||
|
default:
|
||||||
|
return LightThemeName;
|
||||||
|
}
|
||||||
|
} else if (theme === undefined || typeof theme === "undefined") {
|
||||||
|
return LightThemeName;
|
||||||
|
} else {
|
||||||
|
const themeName = "custom-vs";
|
||||||
|
|
||||||
|
// Skip redefining the same custom theme if it is the same theme data.
|
||||||
|
if (customThemeData !== theme) {
|
||||||
|
monaco.editor.defineTheme(themeName, theme);
|
||||||
|
customThemeData = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return themeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMapStateToProps = (initialState: AppState, initialProps: IMonacoProps) => {
|
||||||
|
const { id, contentRef } = initialProps;
|
||||||
|
const mapStateToProps = (state: AppState, ownProps: IMonacoProps & IMonacoStateProps) => {
|
||||||
|
return {
|
||||||
|
language: getCellMonacoLanguage(
|
||||||
|
state,
|
||||||
|
contentRef,
|
||||||
|
id,
|
||||||
|
ownProps.cellLanguageOverride,
|
||||||
|
ownProps.notebookLanguageOverride
|
||||||
|
)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a MonacoEditor instance within the MonacoContainer div
|
||||||
|
*/
|
||||||
|
export class MonacoEditor extends React.Component<IMonacoProps & IMonacoStateProps> {
|
||||||
|
editor?: monaco.editor.IStandaloneCodeEditor;
|
||||||
|
editorContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
contentHeight?: number;
|
||||||
|
private cursorPositionListener?: monaco.IDisposable;
|
||||||
|
|
||||||
|
constructor(props: IMonacoProps & IMonacoStateProps) {
|
||||||
|
super(props);
|
||||||
|
this.onFocus = this.onFocus.bind(this);
|
||||||
|
this.onBlur = this.onBlur.bind(this);
|
||||||
|
this.calculateHeight = this.calculateHeight.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDidChangeModelContent(e: monaco.editor.IModelContentChangedEvent): void {
|
||||||
|
if (this.editor) {
|
||||||
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(this.editor.getValue(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calculateHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjust the height of editor
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The way to determine how many lines we should display in editor:
|
||||||
|
* If numberOfLines is not set or set to 0, we adjust the height to fit the content
|
||||||
|
* If numberOfLines is specified we respect that setting
|
||||||
|
*/
|
||||||
|
calculateHeight(): void {
|
||||||
|
// Make sure we have an editor
|
||||||
|
if (!this.editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we have a model
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||||
|
const expectedLines = this.props.numberOfLines || model.getLineCount();
|
||||||
|
// The find & replace menu takes up 2 lines, that is why 2 line is set as the minimum number of lines
|
||||||
|
// TODO: we should either disable the find/replace menu or auto expand the editor when find/replace is triggerred.
|
||||||
|
const finalizedLines = Math.max(expectedLines, 1) + 1;
|
||||||
|
const lineHeight = this.editor.getConfiguration().lineHeight;
|
||||||
|
|
||||||
|
const contentHeight = finalizedLines * lineHeight;
|
||||||
|
if (this.contentHeight !== contentHeight) {
|
||||||
|
this.editorContainerRef.current.style.height = contentHeight + "px";
|
||||||
|
this.editor.layout();
|
||||||
|
this.contentHeight = contentHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||||
|
// Register Jupyter completion provider if needed
|
||||||
|
this.registerCompletionProvider();
|
||||||
|
|
||||||
|
// Use Monaco model uri if provided. Otherwise, create a new model uri using editor id.
|
||||||
|
const uri = this.props.modelUri ? this.props.modelUri : monaco.Uri.file(this.props.id);
|
||||||
|
|
||||||
|
// Only create a new model if it does not exist. For example, when we double click on a markdown cell,
|
||||||
|
// an editor model is created for it. Once we go back to markdown preview mode that doesn't use the editor,
|
||||||
|
// double clicking on the markdown cell will again instantiate a monaco editor. In that case, we should
|
||||||
|
// rebind the previously created editor model for the markdown instead of recreating one. Monaco does not
|
||||||
|
// allow models to be recreated with the same uri.
|
||||||
|
let model = monaco.editor.getModel(uri);
|
||||||
|
if (!model) {
|
||||||
|
model = monaco.editor.createModel(this.props.value, this.props.language, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Monaco editor backed by a Monaco model.
|
||||||
|
this.editor = monaco.editor.create(this.editorContainerRef.current, {
|
||||||
|
// Following are the default settings
|
||||||
|
minimap: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
autoIndent: true,
|
||||||
|
overviewRulerLanes: 1,
|
||||||
|
scrollbar: {
|
||||||
|
useShadows: false,
|
||||||
|
verticalHasArrows: false,
|
||||||
|
horizontalHasArrows: false,
|
||||||
|
vertical: "hidden",
|
||||||
|
horizontal: "hidden",
|
||||||
|
verticalScrollbarSize: 0,
|
||||||
|
horizontalScrollbarSize: 0,
|
||||||
|
arrowSize: 30
|
||||||
|
},
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
find: {
|
||||||
|
// TODO Need this?
|
||||||
|
// addExtraSpaceOnTop: false, // pops the editor out of alignment if turned on
|
||||||
|
seedSearchStringFromSelection: true, // default is true
|
||||||
|
autoFindInSelection: false // default is false
|
||||||
|
},
|
||||||
|
// Disable highlight current line, too much visual noise with it on.
|
||||||
|
// VS Code also has it disabled for their notebook experience.
|
||||||
|
renderLineHighlight: "none",
|
||||||
|
|
||||||
|
// Allow editor pop up widgets such as context menus, signature help, hover tips to be able to be
|
||||||
|
// displayed outside of the editor. Without this, the pop up widgets can be clipped.
|
||||||
|
fixedOverflowWidgets: true,
|
||||||
|
|
||||||
|
// Apply custom settings from configuration
|
||||||
|
...this.props.options,
|
||||||
|
|
||||||
|
// Apply specific settings passed-in as direct props
|
||||||
|
model,
|
||||||
|
value: this.props.value,
|
||||||
|
language: this.props.language,
|
||||||
|
readOnly: this.props.readOnly,
|
||||||
|
lineNumbers: this.props.lineNumbers ? "on" : "off",
|
||||||
|
theme: getMonacoTheme(this.props.theme)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEditorTopMargin();
|
||||||
|
|
||||||
|
// Ignore Ctrl + Enter
|
||||||
|
// tslint:disable-next-line no-bitwise
|
||||||
|
this.editor.addCommand(
|
||||||
|
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||||
|
() => {
|
||||||
|
// Do nothing. This is handled elsewhere, we just don't want the editor to put the newline.
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
// TODO Add right context
|
||||||
|
|
||||||
|
this.toggleEditorOptions(this.props.editorFocused);
|
||||||
|
|
||||||
|
if (this.props.editorFocused) {
|
||||||
|
if (!this.editor.hasTextFocus()) {
|
||||||
|
// Bring browser focus to the editor if text not already in focus
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
this.registerCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Need to remove the event listener when the editor is disposed, or we have a memory leak here.
|
||||||
|
// The same applies to the other event listeners below
|
||||||
|
// Adds listener under the resize window event which calls the resize method
|
||||||
|
window.addEventListener("resize", this.resize.bind(this));
|
||||||
|
|
||||||
|
// Adds listeners for undo and redo actions emitted from the toolbar
|
||||||
|
this.editorContainerRef.current.addEventListener("undo", () => {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.trigger("undo-event", "undo", {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.editorContainerRef.current.addEventListener("redo", () => {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.trigger("redo-event", "redo", {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editor.onDidChangeModelContent(this.onDidChangeModelContent.bind(this));
|
||||||
|
this.editor.onDidFocusEditorText(this.onFocus);
|
||||||
|
this.editor.onDidBlurEditorText(this.onBlur);
|
||||||
|
this.calculateHeight();
|
||||||
|
|
||||||
|
// FIXME: This might need further investigation as the props value should be respected in construction
|
||||||
|
// The following is a mitigation measure till that time
|
||||||
|
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||||
|
this.editor.setValue(this.props.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEditorTopMargin(): void {
|
||||||
|
if (this.editor) {
|
||||||
|
// Monaco editor doesn't have margins
|
||||||
|
// https://github.com/notable/notable/issues/551
|
||||||
|
// This is a workaround to add an editor area 12px padding at the top
|
||||||
|
// so that cursors rendered by collab decorators could be visible without being cut.
|
||||||
|
this.editor.changeViewZones(changeAccessor => {
|
||||||
|
const domNode = document.createElement("div");
|
||||||
|
changeAccessor.addZone({
|
||||||
|
afterLineNumber: 0,
|
||||||
|
heightInPx: 12,
|
||||||
|
domNode
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells editor to check the surrounding container size and resize itself appropriately
|
||||||
|
*/
|
||||||
|
resize(): void {
|
||||||
|
if (this.editor && this.props.editorFocused) {
|
||||||
|
this.editor.layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(): void {
|
||||||
|
if (!this.editor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value, channels, /* language, contentRef, id,*/ editorFocused, theme } = this.props;
|
||||||
|
|
||||||
|
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||||
|
if (this.editor.getValue() !== value) {
|
||||||
|
this.editor.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
completionProvider.setChannels(channels);
|
||||||
|
|
||||||
|
// Register Jupyter completion provider if needed
|
||||||
|
this.registerCompletionProvider();
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Apply new model to the editor when the language is changed.
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (model && language && model.getModeId() !== language) {
|
||||||
|
const newUri = DocumentUri.createCellUri(contentRef, id, language);
|
||||||
|
if (!monaco.editor.getModel(newUri)) {
|
||||||
|
// Save the cursor position before we set new model.
|
||||||
|
const position = this.editor.getPosition();
|
||||||
|
|
||||||
|
// Set new model targeting the changed language.
|
||||||
|
this.editor.setModel(monaco.editor.createModel(value, language, newUri));
|
||||||
|
this.addEditorTopMargin();
|
||||||
|
|
||||||
|
// Restore cursor position to new model.
|
||||||
|
if (position) {
|
||||||
|
this.editor.setPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose of the old model in a seperate event. We cannot dispose of the model within the
|
||||||
|
// componentDidUpdate method or else the editor will throw an exception. Zero in the timeout field
|
||||||
|
// means execute immediately but in a seperate next event.
|
||||||
|
setTimeout(() => model.dispose(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (theme) {
|
||||||
|
monaco.editor.setTheme(getMonacoTheme(theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the multi-tabs scenario, when the notebook is hidden by setting "display:none",
|
||||||
|
// Any state update propagated here would cause a UI re-layout, monaco-editor will then recalculate
|
||||||
|
// and set its height to 5px.
|
||||||
|
// To work around that issue, we skip updating the UI when parent element's offsetParent is null (which
|
||||||
|
// indicate an ancient element is hidden by display set to none)
|
||||||
|
// We may revisit this when we get to refactor for multi-notebooks.
|
||||||
|
if (!this.editorContainerRef.current?.offsetParent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set focus
|
||||||
|
if (editorFocused && !this.editor.hasTextFocus()) {
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tells the editor pane to check if its container has changed size and fill appropriately
|
||||||
|
this.editor.layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if (this.editor) {
|
||||||
|
try {
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (model) {
|
||||||
|
model.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.dispose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error occurs in disposing editor: ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="monaco-container">
|
||||||
|
<div ref={this.editorContainerRef} id={`editor-${this.props.id}`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register default kernel-based completion provider.
|
||||||
|
* @param language Language
|
||||||
|
*/
|
||||||
|
registerDefaultCompletionProvider(language: string): void {
|
||||||
|
// onLanguage event is emitted only once per language when language is first time needed.
|
||||||
|
monaco.languages.onLanguage(language, () => {
|
||||||
|
monaco.languages.registerCompletionItemProvider(language, completionProvider);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFocus() {
|
||||||
|
this.props.onFocusChange(true);
|
||||||
|
this.toggleEditorOptions(true);
|
||||||
|
this.registerCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBlur() {
|
||||||
|
this.props.onFocusChange(false);
|
||||||
|
this.toggleEditorOptions(false);
|
||||||
|
this.unregisterCursorListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerCursorListener() {
|
||||||
|
if (this.editor && this.props.onCursorPositionChange) {
|
||||||
|
const selection = this.editor.getSelection();
|
||||||
|
this.props.onCursorPositionChange(selection);
|
||||||
|
|
||||||
|
if (!this.cursorPositionListener) {
|
||||||
|
this.cursorPositionListener = this.editor.onDidChangeCursorSelection(event =>
|
||||||
|
this.props.onCursorPositionChange!(event.selection)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unregisterCursorListener() {
|
||||||
|
if (this.cursorPositionListener) {
|
||||||
|
this.cursorPositionListener.dispose();
|
||||||
|
this.cursorPositionListener = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle editor options based on if the editor is in active state (i.e. focused).
|
||||||
|
* When the editor is not active, we want to deactivate some of the visual noise.
|
||||||
|
* @param isActive Whether editor is active.
|
||||||
|
*/
|
||||||
|
private toggleEditorOptions(isActive: boolean) {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.updateOptions({
|
||||||
|
matchBrackets: isActive,
|
||||||
|
occurrencesHighlight: isActive,
|
||||||
|
renderIndentGuides: isActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register language features for target language. Call before setting language type to model.
|
||||||
|
*/
|
||||||
|
private registerCompletionProvider() {
|
||||||
|
const { enableCompletion, language, onRegisterCompletionProvider, shouldRegisterDefaultCompletion } = this.props;
|
||||||
|
|
||||||
|
if (enableCompletion && language) {
|
||||||
|
if (onRegisterCompletionProvider) {
|
||||||
|
onRegisterCompletionProvider(language);
|
||||||
|
} else if (shouldRegisterDefaultCompletion) {
|
||||||
|
this.registerDefaultCompletionProvider(language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect<IMonacoStateProps, void, IMonacoProps, AppState>(makeMapStateToProps)(MonacoEditor);
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
// import * as monaco from "../monaco";
|
||||||
|
import { Observable, Observer } from "rxjs";
|
||||||
|
import { first, map } from "rxjs/operators";
|
||||||
|
import { childOf, JupyterMessage, ofMessageType, Channels } from "@nteract/messaging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: import from nteract when the changes under editor-base.ts are ported to nteract.
|
||||||
|
*/
|
||||||
|
import { CompletionResults, CompletionMatch, completionRequest, js_idx_to_char_idx } from "../editor-base";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter to Monaco completion item kinds.
|
||||||
|
*/
|
||||||
|
const unknownJupyterKind = "<unknown>";
|
||||||
|
const jupyterToMonacoCompletionItemKind = {
|
||||||
|
[unknownJupyterKind]: monaco.languages.CompletionItemKind.Field,
|
||||||
|
class: monaco.languages.CompletionItemKind.Class,
|
||||||
|
function: monaco.languages.CompletionItemKind.Function,
|
||||||
|
keyword: monaco.languages.CompletionItemKind.Keyword,
|
||||||
|
instance: monaco.languages.CompletionItemKind.Variable,
|
||||||
|
statement: monaco.languages.CompletionItemKind.Variable
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completion item provider.
|
||||||
|
*/
|
||||||
|
class CompletionItemProvider implements monaco.languages.CompletionItemProvider {
|
||||||
|
private channels: Channels | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set Channels of Jupyter kernel.
|
||||||
|
* @param channels Channels of Jupyter kernel.
|
||||||
|
*/
|
||||||
|
setChannels(channels: Channels | undefined) {
|
||||||
|
this.channels = channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether provider is connected to Jupyter kernel.
|
||||||
|
*/
|
||||||
|
get isConnectedToKernel() {
|
||||||
|
return !!this.channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional characters to trigger completion other than Ctrl+Space.
|
||||||
|
*/
|
||||||
|
get triggerCharacters() {
|
||||||
|
return [" ", "<", "/", ".", "="];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of completion items at position of cursor.
|
||||||
|
* @param model Monaco editor text model.
|
||||||
|
* @param position Position of cursor.
|
||||||
|
*/
|
||||||
|
async provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
||||||
|
// Convert to zero-based index
|
||||||
|
let cursorPos = model.getOffsetAt(position);
|
||||||
|
const code = model.getValue();
|
||||||
|
cursorPos = js_idx_to_char_idx(cursorPos, code);
|
||||||
|
|
||||||
|
// Get completions from Jupyter kernel if its Channels is connected
|
||||||
|
let items = [];
|
||||||
|
if (this.channels) {
|
||||||
|
try {
|
||||||
|
const message = completionRequest(code, cursorPos);
|
||||||
|
items = await this.codeCompleteObservable(this.channels, message, model).toPromise();
|
||||||
|
} catch (error) {
|
||||||
|
// Temporary log error to console until we settle on how we log in V3
|
||||||
|
// tslint:disable-next-line
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve<monaco.languages.CompletionList>({
|
||||||
|
suggestions: items,
|
||||||
|
incomplete: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of completion items from Jupyter kernel.
|
||||||
|
* @param channels Channels of Jupyter kernel.
|
||||||
|
* @param message Jupyter message for completion request.
|
||||||
|
* @param model Text model.
|
||||||
|
*/
|
||||||
|
private codeCompleteObservable(channels: Channels, message: JupyterMessage, model: monaco.editor.ITextModel) {
|
||||||
|
// Process completion response
|
||||||
|
const completion$ = channels.pipe(
|
||||||
|
childOf(message),
|
||||||
|
ofMessageType("complete_reply"),
|
||||||
|
map(entry => entry.content),
|
||||||
|
first(),
|
||||||
|
map(results => this.adaptToMonacoCompletions(results, model))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe and send completion request message
|
||||||
|
return Observable.create((observer: Observer<unknown>) => {
|
||||||
|
const subscription = completion$.subscribe(observer);
|
||||||
|
channels.next(message);
|
||||||
|
return subscription;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Jupyter completion result to list of Monaco completion items.
|
||||||
|
*/
|
||||||
|
private adaptToMonacoCompletions(results: CompletionResults, model: monaco.editor.ITextModel) {
|
||||||
|
let range: monaco.IRange;
|
||||||
|
let percentCount = 0;
|
||||||
|
let matches = results ? results.matches : [];
|
||||||
|
if (results.metadata && results.metadata._jupyter_types_experimental) {
|
||||||
|
matches = results.metadata._jupyter_types_experimental as CompletionMatch[];
|
||||||
|
}
|
||||||
|
return matches.map((match: CompletionMatch, index: number) => {
|
||||||
|
if (typeof match === "string") {
|
||||||
|
const text = this.sanitizeText(match);
|
||||||
|
const filtered = this.getFilterText(text);
|
||||||
|
return {
|
||||||
|
kind: this.adaptToMonacoCompletionItemKind(unknownJupyterKind),
|
||||||
|
label: text,
|
||||||
|
insertText: text,
|
||||||
|
filterText: filtered,
|
||||||
|
sortText: this.getSortText(index)
|
||||||
|
} as monaco.languages.CompletionItem;
|
||||||
|
} else {
|
||||||
|
// We only need to get the range once as the range is the same for all completion items in the list.
|
||||||
|
if (!range) {
|
||||||
|
const start = model.getPositionAt(match.start);
|
||||||
|
const end = model.getPositionAt(match.end);
|
||||||
|
range = {
|
||||||
|
startLineNumber: start.lineNumber,
|
||||||
|
startColumn: start.column,
|
||||||
|
endLineNumber: end.lineNumber,
|
||||||
|
endColumn: end.column
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the range representing the text before the completion action was invoked.
|
||||||
|
// If the text starts with magics % indicator, we need to track how many of these indicators exist
|
||||||
|
// so that we ensure the insertion text only inserts the delta between what the user typed versus
|
||||||
|
// what is recommended by the completion. Without this, there will be extra % insertions.
|
||||||
|
// Example:
|
||||||
|
// User types %%p then suggestion list will recommend %%python, if we now commit the item then the
|
||||||
|
// final text in the editor becomes %%p%%python instead of %%python. This is why the tracking code
|
||||||
|
// below is needed. This behavior is only specific to the magics % indicators as Monaco does not
|
||||||
|
// handle % characters in their completion list well.
|
||||||
|
const rangeText = model.getValueInRange(range);
|
||||||
|
if (rangeText.startsWith("%%")) {
|
||||||
|
percentCount = 2;
|
||||||
|
} else if (rangeText.startsWith("%")) {
|
||||||
|
percentCount = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.sanitizeText(match.text);
|
||||||
|
const filtered = this.getFilterText(text);
|
||||||
|
const insert = this.getInsertText(text, percentCount);
|
||||||
|
return {
|
||||||
|
kind: this.adaptToMonacoCompletionItemKind(match.type as keyof typeof jupyterToMonacoCompletionItemKind),
|
||||||
|
label: text,
|
||||||
|
insertText: percentCount > 0 ? insert : text,
|
||||||
|
filterText: filtered,
|
||||||
|
sortText: this.getSortText(index)
|
||||||
|
} as monaco.languages.CompletionItem;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Jupyter completion item kind to Monaco completion item kind.
|
||||||
|
* @param kind Jupyter completion item kind.
|
||||||
|
*/
|
||||||
|
private adaptToMonacoCompletionItemKind(kind: keyof typeof jupyterToMonacoCompletionItemKind) {
|
||||||
|
const result = jupyterToMonacoCompletionItemKind[kind];
|
||||||
|
return result ? result : jupyterToMonacoCompletionItemKind[unknownJupyterKind];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove everything before a dot. Jupyter completion results like to include all characters before
|
||||||
|
* the trigger character. For example, if user types "myarray.", we expect the completion results to
|
||||||
|
* show "append", "pop", etc. but for the actual case, it will show "myarray.append", "myarray.pop",
|
||||||
|
* etc. so we are going to sanitize the text.
|
||||||
|
* @param text Text of Jupyter completion item
|
||||||
|
*/
|
||||||
|
private sanitizeText(text: string) {
|
||||||
|
const index = text.lastIndexOf(".");
|
||||||
|
return index > -1 && index < text.length - 1 ? text.substring(index + 1) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove magics all % characters as Monaco doesn't like them for the filtering text.
|
||||||
|
* Without this, completion won't show magics match items.
|
||||||
|
* @param text Text of Jupyter completion item.
|
||||||
|
*/
|
||||||
|
private getFilterText(text: string) {
|
||||||
|
return text.replace(/%/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get insertion text handling what to insert for the magics case depending on what
|
||||||
|
* has already been typed.
|
||||||
|
* @param text Text of Jupyter completion item.
|
||||||
|
* @param percentCount Number of percent characters to remove
|
||||||
|
*/
|
||||||
|
private getInsertText(text: string, percentCount: number) {
|
||||||
|
for (let i = 0; i < percentCount; i++) {
|
||||||
|
text = text.replace("%", "");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps numbers to strings, such that if a>b numerically, f(a)>f(b) lexicograhically.
|
||||||
|
* 1 -> "za", 26 -> "zz", 27 -> "zza", 28 -> "zzb", 52 -> "zzz", 53 ->"zzza"
|
||||||
|
* @param order Number to be converted to a sorting-string. order >= 0.
|
||||||
|
* @returns A string representing the order.
|
||||||
|
*/
|
||||||
|
private getSortText(order: number): string {
|
||||||
|
order++;
|
||||||
|
const numCharacters = 26; // "z" - "a" + 1;
|
||||||
|
const div = Math.floor(order / numCharacters);
|
||||||
|
|
||||||
|
let sortText = "z";
|
||||||
|
for (let i = 0; i < div; i++) {
|
||||||
|
sortText += "z";
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainder = order % numCharacters;
|
||||||
|
if (remainder > 0) {
|
||||||
|
sortText += String.fromCharCode(96 + remainder);
|
||||||
|
}
|
||||||
|
return sortText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionProvider = new CompletionItemProvider();
|
||||||
|
export { completionProvider };
|
||||||
44
src/Explorer/Notebook/MonacoEditor/converter.ts
Normal file
44
src/Explorer/Notebook/MonacoEditor/converter.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Immutable from "immutable";
|
||||||
|
import * as monaco from "./monaco";
|
||||||
|
/**
|
||||||
|
* Code Mirror to Monaco constants.
|
||||||
|
*/
|
||||||
|
export enum Mode {
|
||||||
|
markdown = "markdown",
|
||||||
|
raw = "plaintext",
|
||||||
|
python = "python",
|
||||||
|
csharp = "csharp"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Code Mirror mode to a valid Monaco Editor supported langauge
|
||||||
|
* defaults to plaintext if map not found.
|
||||||
|
* @param mode Code Mirror mode
|
||||||
|
* @returns Monaco language
|
||||||
|
*/
|
||||||
|
export function mapCodeMirrorModeToMonaco(mode: string | { name: string }): string {
|
||||||
|
let language = "";
|
||||||
|
|
||||||
|
// Parse codemirror mode object
|
||||||
|
if (typeof mode === "string") {
|
||||||
|
language = mode;
|
||||||
|
}
|
||||||
|
// Vanilla object
|
||||||
|
else if (typeof mode === "object" && mode.name) {
|
||||||
|
language = mode.name;
|
||||||
|
}
|
||||||
|
// Immutable Map
|
||||||
|
else if (Immutable.Map.isMap(mode) && mode.has("name")) {
|
||||||
|
language = mode.get("name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to handle "ipython" as a special case since it is not a registered language
|
||||||
|
if (language === "ipython") {
|
||||||
|
return Mode.python;
|
||||||
|
} else if (language === "text/x-csharp") {
|
||||||
|
return Mode.csharp;
|
||||||
|
} else if (monaco.languages.getEncodedLanguageId(language) > 0) {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
return Mode.raw;
|
||||||
|
}
|
||||||
76
src/Explorer/Notebook/MonacoEditor/editor-base.ts
Normal file
76
src/Explorer/Notebook/MonacoEditor/editor-base.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Disable linting on file since we will be moving the code below to nteract which have different rules configured.
|
||||||
|
// tslint:disable:variable-name
|
||||||
|
// tslint:disable:interface-name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Create new editor-base package in nteract repo and move all code below to new package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createMessage } from "@nteract/messaging";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter messaging protocol's _jupyter_types_experimental completion result.
|
||||||
|
*/
|
||||||
|
interface CompletionResult {
|
||||||
|
end: number;
|
||||||
|
start: number;
|
||||||
|
type: string;
|
||||||
|
text: string;
|
||||||
|
displayText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Juptyer completion match item.
|
||||||
|
*/
|
||||||
|
export type CompletionMatch = string | CompletionResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jupyter messaging protocol's complete_reply response.
|
||||||
|
*/
|
||||||
|
export interface CompletionResults {
|
||||||
|
status: string;
|
||||||
|
cursor_start: number;
|
||||||
|
cursor_end: number;
|
||||||
|
matches: CompletionMatch[];
|
||||||
|
metadata?: {
|
||||||
|
_jupyter_types_experimental?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Jupyter messaging protocol's complete_request message.
|
||||||
|
* @param code Code of editor.
|
||||||
|
* @param cursorPos cursor position represented in the Jupyter messaging protocol (character position)
|
||||||
|
*/
|
||||||
|
export const completionRequest = (code: string, cursorPos: number) =>
|
||||||
|
createMessage("complete_request", {
|
||||||
|
content: {
|
||||||
|
code,
|
||||||
|
cursor_pos: cursorPos
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaScript stores text as utf16 and string indices use "code units",
|
||||||
|
* which stores high-codepoint characters as "surrogate pairs",
|
||||||
|
* which occupy two indices in the JavaScript string.
|
||||||
|
* We need to translate cursor_pos in the protocol (in characters)
|
||||||
|
* to js offset (with surrogate pairs taking two spots).
|
||||||
|
* @param js_idx JavaScript index
|
||||||
|
* @param text Text
|
||||||
|
*/
|
||||||
|
export const js_idx_to_char_idx: (js_idx: number, text: string) => number = (js_idx: number, text: string): number => {
|
||||||
|
let char_idx: number = js_idx;
|
||||||
|
for (let i = 0; i + 1 < text.length && i < js_idx; i++) {
|
||||||
|
const char_code: number = text.charCodeAt(i);
|
||||||
|
// check for surrogate pair
|
||||||
|
if (char_code >= 0xd800 && char_code <= 0xdbff) {
|
||||||
|
const next_char_code: number = text.charCodeAt(i + 1);
|
||||||
|
if (next_char_code >= 0xdc00 && next_char_code <= 0xdfff) {
|
||||||
|
char_idx--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return char_idx;
|
||||||
|
};
|
||||||
10
src/Explorer/Notebook/MonacoEditor/monaco.ts
Normal file
10
src/Explorer/Notebook/MonacoEditor/monaco.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export * from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Set the custom worker url to workaround the cross-domain issue with creating web worker
|
||||||
|
// * See https://github.com/microsoft/monaco-editor/blob/master/docs/integrate-amd-cross.md for more details
|
||||||
|
// * This step has to be executed after a importing of monaco-editor once per chunk to make sure
|
||||||
|
// * the custom worker url overwrites the one from monaco-editor module itself.
|
||||||
|
// */
|
||||||
|
// import { setMonacoWorkerUrl } from "./workerUrl";
|
||||||
|
// setMonacoWorkerUrl();
|
||||||
67
src/Explorer/Notebook/MonacoEditor/selectors.ts
Normal file
67
src/Explorer/Notebook/MonacoEditor/selectors.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { AppState, ContentRef, selectors as nteractSelectors } from "@nteract/core";
|
||||||
|
import { CellId } from "@nteract/commutable";
|
||||||
|
import { Mode, mapCodeMirrorModeToMonaco } from "./converter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given cell, falling back to the notebook language if one for the cell is not defined.
|
||||||
|
*/
|
||||||
|
export const getCellMonacoLanguage = (
|
||||||
|
state: AppState,
|
||||||
|
contentRef: ContentRef,
|
||||||
|
cellId: CellId,
|
||||||
|
cellLanguageOverride?: string,
|
||||||
|
notebookLanguageOverride?: string
|
||||||
|
): string => {
|
||||||
|
const model = nteractSelectors.model(state, { contentRef });
|
||||||
|
if (!model || model.type !== "notebook") {
|
||||||
|
throw new Error("Connected Editor components should not be used with non-notebook models");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = nteractSelectors.notebook.cellById(model, { id: cellId });
|
||||||
|
if (!cell) {
|
||||||
|
throw new Error("Invalid cell id");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cell.cell_type) {
|
||||||
|
case "markdown":
|
||||||
|
return Mode.markdown;
|
||||||
|
case "raw":
|
||||||
|
return Mode.raw;
|
||||||
|
case "code":
|
||||||
|
if (cellLanguageOverride) {
|
||||||
|
return mapCodeMirrorModeToMonaco(cellLanguageOverride);
|
||||||
|
} else {
|
||||||
|
// Fall back to notebook language if cell language isn't present.
|
||||||
|
return getNotebookMonacoLanguage(state, contentRef, notebookLanguageOverride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language to use for syntax highlighting and autocompletion in the Monaco Editor for a given notebook.
|
||||||
|
*/
|
||||||
|
export const getNotebookMonacoLanguage = (
|
||||||
|
state: AppState,
|
||||||
|
contentRef: ContentRef,
|
||||||
|
notebookLanguageOverride?: string
|
||||||
|
): string => {
|
||||||
|
const model = nteractSelectors.model(state, { contentRef });
|
||||||
|
if (!model || model.type !== "notebook") {
|
||||||
|
throw new Error("Connected Editor components should not be used with non-notebook models");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notebookLanguageOverride) {
|
||||||
|
return mapCodeMirrorModeToMonaco(notebookLanguageOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kernelRef = model.kernelRef;
|
||||||
|
let codeMirrorMode;
|
||||||
|
// Try to get the CodeMirror mode from the kernel.
|
||||||
|
if (kernelRef) {
|
||||||
|
codeMirrorMode = nteractSelectors.kernel(state, { kernelRef })?.info?.codemirrorMode;
|
||||||
|
}
|
||||||
|
// As a fallback, get the CodeMirror mode from the notebook itself.
|
||||||
|
codeMirrorMode = codeMirrorMode ?? nteractSelectors.notebook.codeMirrorMode(model);
|
||||||
|
|
||||||
|
return mapCodeMirrorModeToMonaco(codeMirrorMode);
|
||||||
|
};
|
||||||
22
src/Explorer/Notebook/MonacoEditor/styles.css
Normal file
22
src/Explorer/Notebook/MonacoEditor/styles.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
For the following components, we use the inherited width values from monaco-container.
|
||||||
|
On resizing the browser, the width of monaco-container will be calculated
|
||||||
|
and we just use the calculated width for the following components
|
||||||
|
So we don't need to use Monaco editor's layout() function which is expensive operation and causes performance issues on resizing.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
TODO: These styles below are added for resizing perf improvement.
|
||||||
|
Once the virtualization is implemented, we will revisit this later.
|
||||||
|
*/
|
||||||
|
.monaco-container .monaco-editor {
|
||||||
|
width: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-container .monaco-editor .overflow-guard {
|
||||||
|
width: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 26px is the left margin for .monaco-scrollable-element */
|
||||||
|
.monaco-container .monaco-editor .monaco-scrollable-element.editor-scrollable.vs {
|
||||||
|
width: calc(100% - 26px) !important;
|
||||||
|
}
|
||||||
75
src/Explorer/Notebook/MonacoEditor/theme.ts
Normal file
75
src/Explorer/Notebook/MonacoEditor/theme.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import * as monaco from "./monaco";
|
||||||
|
|
||||||
|
// TODO: move defineTheme calls to an initialization function
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default light theme with customized background
|
||||||
|
*/
|
||||||
|
export const LightThemeName = "vs-light";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for light theme
|
||||||
|
*/
|
||||||
|
export const customMonacoLightTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs", // Derive from default light theme of Monaco
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
// We want Monaco background to use the same background for our themes.
|
||||||
|
// Without this, the Monaco light theme has a yellowish tone.
|
||||||
|
// Verified with UX that white meets all the accessbility requirements for light
|
||||||
|
// and high contrast light theme.
|
||||||
|
"editor.background": "#FFFFFF"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(LightThemeName, customMonacoLightTheme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default dark theme with customized background
|
||||||
|
*/
|
||||||
|
export const DarkThemeName = "aznb-dark";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for dark theme
|
||||||
|
*/
|
||||||
|
export const customMonacoDarkTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs-dark", // Derive from default dark theme of Monaco
|
||||||
|
inherit: true,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
"editor.background": "#1b1a19"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(DarkThemeName, customMonacoDarkTheme);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The custom high contrast light theme with customized background
|
||||||
|
*/
|
||||||
|
export const HCLightThemeName = "hc-light";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default monaco theme for light high contrast mode
|
||||||
|
*/
|
||||||
|
export const customMonacoHCLightTheme: monaco.editor.IStandaloneThemeData = {
|
||||||
|
base: "vs", // Derive from default light theme of Monaco; change all grey colors to black to comply with highcontrast rules
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: "annotation", foreground: "000000" },
|
||||||
|
{ token: "delimiter.html", foreground: "000000" },
|
||||||
|
{ token: "operator.scss", foreground: "000000" },
|
||||||
|
{ token: "operator.sql", foreground: "000000" },
|
||||||
|
{ token: "operator.swift", foreground: "000000" },
|
||||||
|
{ token: "predefined.sql", foreground: "000000" }
|
||||||
|
],
|
||||||
|
colors: {
|
||||||
|
// We want Monaco background to use the same background for our themes.
|
||||||
|
// Without this, the Monaco light theme has a yellowish tone.
|
||||||
|
// Verified with UX that white meets all the accessbility requirements for light
|
||||||
|
// and high contrast light theme.
|
||||||
|
"editor.background": "#FFFFFF"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.editor.defineTheme(HCLightThemeName, customMonacoHCLightTheme);
|
||||||
@@ -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: createKernelRef(),
|
kernelRef: undefined, // must be undefined or it will be auto-started by the epic
|
||||||
contentRef: this.contentRef
|
contentRef: this.contentRef
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 } from "rxjs";
|
import { Subject, empty } 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 } from "./epics";
|
import { launchWebSocketKernelEpic, autoStartKernelEpic } from "./epics";
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
import { NotebookUtil } from "../NotebookUtil";
|
||||||
|
|
||||||
import { sessions } from "rx-jupyter";
|
import { sessions } from "rx-jupyter";
|
||||||
@@ -74,46 +74,47 @@ describe("Extract kernel from notebook", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
app: state.makeAppRecord({
|
||||||
|
host: state.makeJupyterHostRecord({
|
||||||
|
type: "jupyter",
|
||||||
|
token: "eh",
|
||||||
|
basePath: "/"
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
comms: state.makeCommsRecord(),
|
||||||
|
config: Immutable.Map({}),
|
||||||
|
core: state.makeStateRecord({
|
||||||
|
kernelRef: "fake",
|
||||||
|
entities: state.makeEntitiesRecord({
|
||||||
|
contents: state.makeContentsRecord({
|
||||||
|
byRef: Immutable.Map({
|
||||||
|
fakeContentRef: state.makeNotebookContentRecord()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
kernels: state.makeKernelsRecord({
|
||||||
|
byRef: Immutable.Map({
|
||||||
|
fake: state.makeRemoteKernelRecord({
|
||||||
|
type: "websocket",
|
||||||
|
channels: new Subject<any>(),
|
||||||
|
kernelSpecName: "fancy",
|
||||||
|
id: "0"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
cdb: makeCdbRecord({
|
||||||
|
databaseAccountName: "dbAccountName",
|
||||||
|
defaultExperience: "defaultExperience"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
describe("launchWebSocketKernelEpic", () => {
|
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);
|
||||||
@@ -490,3 +491,55 @@ describe("launchWebSocketKernelEpic", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("autoStartKernelEpic", () => {
|
||||||
|
const contentRef = "fakeContentRef";
|
||||||
|
const kernelRef = "fake";
|
||||||
|
|
||||||
|
it("automatically starts kernel when content fetch is successful if kernelRef is defined", async () => {
|
||||||
|
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
|
||||||
|
|
||||||
|
const action$ = ActionsObservable.of(
|
||||||
|
actions.fetchContentFulfilled({
|
||||||
|
contentRef,
|
||||||
|
kernelRef,
|
||||||
|
filepath: "filepath",
|
||||||
|
model: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseActions = await autoStartKernelEpic(action$, state$)
|
||||||
|
.pipe(toArray())
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
expect(responseActions).toMatchObject([
|
||||||
|
{
|
||||||
|
type: actions.RESTART_KERNEL,
|
||||||
|
payload: {
|
||||||
|
contentRef,
|
||||||
|
kernelRef,
|
||||||
|
outputHandling: "None"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Don't start kernel when content fetch is successful if kernelRef is not defined", async () => {
|
||||||
|
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
|
||||||
|
|
||||||
|
const action$ = ActionsObservable.of(
|
||||||
|
actions.fetchContentFulfilled({
|
||||||
|
contentRef,
|
||||||
|
kernelRef: undefined,
|
||||||
|
filepath: "filepath",
|
||||||
|
model: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseActions = await autoStartKernelEpic(action$, state$)
|
||||||
|
.pipe(toArray())
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
expect(responseActions).toMatchObject([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { empty, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
|
import { EMPTY, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
|
||||||
import { webSocket } from "rxjs/webSocket";
|
import { webSocket } from "rxjs/webSocket";
|
||||||
import { ActionsObservable, StateObservable } from "redux-observable";
|
import { ActionsObservable, StateObservable } from "redux-observable";
|
||||||
import { ofType } from "redux-observable";
|
import { ofType } from "redux-observable";
|
||||||
@@ -77,7 +77,7 @@ const addInitialCodeCellEpic = (
|
|||||||
|
|
||||||
// If it's not a notebook, we shouldn't be here
|
// If it's not a notebook, we shouldn't be here
|
||||||
if (!model || model.type !== "notebook") {
|
if (!model || model.type !== "notebook") {
|
||||||
return empty();
|
return EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(model);
|
const cellOrder = selectors.notebook.cellOrder(model);
|
||||||
@@ -90,7 +90,40 @@ const addInitialCodeCellEpic = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return empty();
|
return EMPTY;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically start kernel if kernelRef is present.
|
||||||
|
* The kernel is normally lazy-started when a cell is being executed, but a running kernel is
|
||||||
|
* required for code completion to work.
|
||||||
|
* For notebook viewer, there is no kernel
|
||||||
|
* @param action$
|
||||||
|
* @param state$
|
||||||
|
*/
|
||||||
|
export const autoStartKernelEpic = (
|
||||||
|
action$: ActionsObservable<actions.FetchContentFulfilled>,
|
||||||
|
state$: StateObservable<AppState>
|
||||||
|
): Observable<{} | actions.CreateCellBelow> => {
|
||||||
|
return action$.pipe(
|
||||||
|
ofType(actions.FETCH_CONTENT_FULFILLED),
|
||||||
|
mergeMap(action => {
|
||||||
|
const state = state$.value;
|
||||||
|
const { contentRef, kernelRef } = action.payload;
|
||||||
|
|
||||||
|
if (!kernelRef) {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(
|
||||||
|
actions.restartKernel({
|
||||||
|
contentRef,
|
||||||
|
kernelRef,
|
||||||
|
outputHandling: "None"
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -288,7 +321,7 @@ export const launchWebSocketKernelEpic = (
|
|||||||
const state = state$.value;
|
const 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();
|
||||||
@@ -299,7 +332,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;
|
||||||
@@ -513,26 +546,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,
|
||||||
@@ -593,7 +626,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);
|
||||||
@@ -608,7 +641,7 @@ const focusInitialCodeCellEpic = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return empty();
|
return EMPTY;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -661,7 +694,7 @@ const notificationsToUserEpic = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return empty();
|
return EMPTY;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -701,7 +734,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(
|
||||||
@@ -814,7 +847,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;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -842,13 +875,14 @@ 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,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { CellType } from "@nteract/commutable/src";
|
|||||||
import "./NotebookRenderer.less";
|
import "./NotebookRenderer.less";
|
||||||
import HoverableCell from "./decorators/HoverableCell";
|
import HoverableCell from "./decorators/HoverableCell";
|
||||||
import CellLabeler from "./decorators/CellLabeler";
|
import CellLabeler from "./decorators/CellLabeler";
|
||||||
|
import MonacoEditor from "../MonacoEditor/MonacoEditor";
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
|
|
||||||
export interface NotebookRendererBaseProps {
|
export interface NotebookRendererBaseProps {
|
||||||
@@ -116,7 +117,12 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
{{
|
{{
|
||||||
editor: {
|
editor: {
|
||||||
codemirror: (props: PassedEditorProps) => (
|
codemirror: (props: PassedEditorProps) => (
|
||||||
<CodeMirrorEditor {...props} lineNumbers={true} />
|
<MonacoEditor
|
||||||
|
{...props}
|
||||||
|
lineNumbers={true}
|
||||||
|
enableCompletion={true}
|
||||||
|
shouldRegisterDefaultCompletion={true}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
|||||||
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
|
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
|
||||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
|
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
|
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class AddDatabasePane extends ContextualPaneBase {
|
export default class AddDatabasePane extends ContextualPaneBase {
|
||||||
@@ -304,76 +304,23 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
this.formErrors("");
|
this.formErrors("");
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
|
|
||||||
const createDatabaseParameters: DataModels.RpParameters = {
|
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||||
db: addDatabasePaneStartMessage.database.id,
|
autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(),
|
||||||
st: addDatabasePaneStartMessage.database.shared,
|
databaseId: addDatabasePaneStartMessage.database.id,
|
||||||
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
|
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
|
||||||
sid: userContext.subscriptionId,
|
offerThroughput: addDatabasePaneStartMessage.offerThroughput
|
||||||
rg: userContext.resourceGroup,
|
|
||||||
dba: addDatabasePaneStartMessage.databaseAccountName
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const autopilotSettings = this._getAutopilotSettings();
|
createDatabase(createDatabaseParams).then(
|
||||||
|
(database: DataModels.Database) => {
|
||||||
if (this.container.isPreferredApiCassandra()) {
|
this._onCreateDatabaseSuccess(offerThroughput, startKey);
|
||||||
this._createKeyspace(createDatabaseParameters, autopilotSettings, startKey);
|
},
|
||||||
} else if (this.container.isPreferredApiMongoDB() && EnvironmentUtility.isAadUser()) {
|
(reason: any) => {
|
||||||
this._createMongoDatabase(createDatabaseParameters, autopilotSettings, startKey);
|
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
|
||||||
} 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());
|
||||||
@@ -396,71 +343,6 @@ 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(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(
|
|
||||||
createKeyspaceParameters: DataModels.RpParameters,
|
|
||||||
autoPilotSettings: DataModels.RpOptions,
|
|
||||||
startKey: number
|
|
||||||
): void {
|
|
||||||
AddDbUtilities.createCassandraKeyspace(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();
|
||||||
@@ -581,20 +463,6 @@ 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);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface GenericRightPaneProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
submitButtonText: string;
|
submitButtonText: string;
|
||||||
title: string;
|
title: string;
|
||||||
isSubmitButtonVisible?: boolean;
|
isSubmitButtonHidden?: 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.isSubmitButtonVisible ? "visible" : "hidden" }}
|
style={{ visibility: this.props.isSubmitButtonHidden ? "hidden" : "visible" }}
|
||||||
ariaLabel="Submit"
|
ariaLabel="Submit"
|
||||||
title="Submit"
|
title="Submit"
|
||||||
onClick={this.props.onSubmit}
|
onClick={this.props.onSubmit}
|
||||||
|
|||||||
@@ -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(),
|
||||||
isSubmitButtonVisible: this.isCodeOfConductAccepted
|
isSubmitButtonHidden: !this.isCodeOfConductAccepted
|
||||||
};
|
};
|
||||||
|
|
||||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
<GalleryCardComponent
|
<GalleryCardComponent
|
||||||
data={{
|
data={{
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: this.props.notebookName,
|
name: this.state.notebookName,
|
||||||
description: this.state.notebookDescription,
|
description: this.state.notebookDescription,
|
||||||
gitSha: undefined,
|
gitSha: undefined,
|
||||||
tags: this.state.notebookTags.split(","),
|
tags: this.state.notebookTags.split(","),
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ 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(),
|
||||||
|
|||||||
@@ -147,6 +147,30 @@ 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,
|
||||||
@@ -156,34 +180,17 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
ariaLabel: saveLabel,
|
ariaLabel: saveLabel,
|
||||||
children: this.container.isGalleryPublishEnabled()
|
children: saveButtonChildren.length && [
|
||||||
? [
|
{
|
||||||
{
|
iconName: "Save",
|
||||||
iconName: "Save",
|
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
commandButtonLabel: saveLabel,
|
||||||
commandButtonLabel: saveLabel,
|
hasPopup: false,
|
||||||
hasPopup: false,
|
disabled: false,
|
||||||
disabled: false,
|
ariaLabel: saveLabel
|
||||||
ariaLabel: saveLabel
|
},
|
||||||
},
|
...saveButtonChildren
|
||||||
{
|
]
|
||||||
iconName: "Copy",
|
|
||||||
onCommandClick: () => this.copyNotebook(),
|
|
||||||
commandButtonLabel: copyToLabel,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: false,
|
|
||||||
ariaLabel: copyToLabel
|
|
||||||
},
|
|
||||||
{
|
|
||||||
iconName: "PublishContent",
|
|
||||||
onCommandClick: async () => await this.publishToGallery(),
|
|
||||||
commandButtonLabel: publishLabel,
|
|
||||||
hasPopup: false,
|
|
||||||
disabled: false,
|
|
||||||
ariaLabel: publishLabel
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: undefined
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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 = {
|
||||||
@@ -16,7 +17,7 @@ describe("Settings tab", () => {
|
|||||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||||
conflictResolutionPath: "/_ts"
|
conflictResolutionPath: "/_ts"
|
||||||
},
|
},
|
||||||
indexingPolicy: {},
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
_rid: "",
|
_rid: "",
|
||||||
_self: "",
|
_self: "",
|
||||||
_etag: "",
|
_etag: "",
|
||||||
@@ -51,7 +52,7 @@ describe("Settings tab", () => {
|
|||||||
defaultTtl: 200,
|
defaultTtl: 200,
|
||||||
partitionKey: null,
|
partitionKey: null,
|
||||||
conflictResolutionPolicy: null,
|
conflictResolutionPolicy: null,
|
||||||
indexingPolicy: {},
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
_rid: "",
|
_rid: "",
|
||||||
_self: "",
|
_self: "",
|
||||||
_etag: "",
|
_etag: "",
|
||||||
@@ -345,7 +346,6 @@ describe("Settings tab", () => {
|
|||||||
|
|
||||||
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 +367,7 @@ describe("Settings tab", () => {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
conflictResolutionPolicy: conflictResolutionPolicy,
|
conflictResolutionPolicy: conflictResolutionPolicy,
|
||||||
indexingPolicy: indexingPolicy,
|
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||||
_rid: "",
|
_rid: "",
|
||||||
_self: "",
|
_self: "",
|
||||||
_etag: "",
|
_etag: "",
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ 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, updateCollection } from "../../Common/DocumentClientUtilityBase";
|
import { updateOffer } 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";
|
||||||
@@ -1009,8 +1010,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSaveClick = (): Q.Promise<any> => {
|
public onSaveClick = async (): Promise<any> => {
|
||||||
let promises: Q.Promise<void>[] = [];
|
|
||||||
this.isExecutionError(false);
|
this.isExecutionError(false);
|
||||||
|
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
@@ -1023,50 +1023,60 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
|
|
||||||
const newCollectionAttributes: any = {};
|
const newCollectionAttributes: any = {};
|
||||||
|
|
||||||
if (this.shouldUpdateCollection()) {
|
try {
|
||||||
let defaultTtl: number;
|
if (this.shouldUpdateCollection()) {
|
||||||
switch (this.timeToLive()) {
|
let defaultTtl: number;
|
||||||
case "on":
|
switch (this.timeToLive()) {
|
||||||
defaultTtl = Number(this.timeToLiveSeconds());
|
case "on":
|
||||||
break;
|
defaultTtl = Number(this.timeToLiveSeconds());
|
||||||
case "on-nodefault":
|
break;
|
||||||
defaultTtl = -1;
|
case "on-nodefault":
|
||||||
break;
|
defaultTtl = -1;
|
||||||
case "off":
|
break;
|
||||||
default:
|
case "off":
|
||||||
defaultTtl = undefined;
|
default:
|
||||||
break;
|
defaultTtl = undefined;
|
||||||
}
|
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.analyticalStorageTtl = this.isAnalyticalStorageEnabled
|
newCollectionAttributes.geospatialConfig = {
|
||||||
? this.analyticalStorageTtlSelection() === "on"
|
type: this.geospatialConfigType()
|
||||||
? Number(this.analyticalStorageTtlSeconds())
|
};
|
||||||
: Constants.AnalyticalStorageTtl.Infinite
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
newCollectionAttributes.geospatialConfig = {
|
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
||||||
type: this.geospatialConfigType()
|
if (!!conflictResolutionChanges) {
|
||||||
};
|
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
|
||||||
|
}
|
||||||
|
|
||||||
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
const newCollection: DataModels.Collection = _.extend(
|
||||||
if (!!conflictResolutionChanges) {
|
{},
|
||||||
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
|
this.collection.rawDataModel,
|
||||||
}
|
newCollectionAttributes
|
||||||
|
);
|
||||||
|
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id(),
|
||||||
|
newCollection
|
||||||
|
);
|
||||||
|
|
||||||
const newCollection: DataModels.Collection = _.extend({}, this.collection.rawDataModel, newCollectionAttributes);
|
if (updatedCollection) {
|
||||||
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);
|
||||||
@@ -1076,164 +1086,133 @@ 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.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.throughput.editableIsDirty() ||
|
||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.rupm.editableIsDirty() ||
|
||||||
this.container != null
|
this._isAutoPilotDirty() ||
|
||||||
|
this._hasProvisioningTypeChanged()
|
||||||
) {
|
) {
|
||||||
const requestPayload = {
|
const newThroughput = this.throughput();
|
||||||
subscriptionId: userContext.subscriptionId,
|
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
|
||||||
resourceGroup: userContext.resourceGroup,
|
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
|
||||||
};
|
|
||||||
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
|
|
||||||
() => {
|
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.throughput(originalThroughputValue);
|
|
||||||
this.notificationStatusInfo(
|
|
||||||
throughputApplyDelayedMessage(
|
|
||||||
this.isAutoPilotSelected(),
|
|
||||||
originalThroughputValue,
|
|
||||||
this._getThroughputUnit(),
|
|
||||||
this.collection.databaseId,
|
|
||||||
this.collection.id(),
|
|
||||||
newThroughput
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.throughput.valueHasMutated(); // force component re-render
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.UpdateSettings,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
databaseName: this.collection && this.collection.databaseId,
|
|
||||||
collectionName: this.collection && this.collection.id(),
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle(),
|
|
||||||
error: error
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
promises.push(Q(updateOfferBeyondLimitPromise));
|
|
||||||
} else {
|
|
||||||
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
|
|
||||||
(updatedOffer: DataModels.Offer) => {
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.collection.offer.valueHasMutated();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
promises.push(updateOfferPromise);
|
if (newOffer.content) {
|
||||||
}
|
newOffer.content.offerThroughput = newThroughput;
|
||||||
}
|
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
|
||||||
|
} else {
|
||||||
if (promises.length === 0) {
|
newOffer = _.extend({}, newOffer, {
|
||||||
this.isExecuting(false);
|
content: {
|
||||||
}
|
offerThroughput: newThroughput,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||||
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));
|
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||||
|
|
||||||
|
if (this.isAutoPilotSelected()) {
|
||||||
|
if (!this.hasAutoPilotV2FeatureFlag()) {
|
||||||
|
newOffer.content.offerAutopilotSettings = {
|
||||||
|
maxThroughput: this.autoPilotThroughput()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newOffer.content.offerAutopilotSettings = {
|
||||||
|
tier: this.selectedAutoPilotTier()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// user has changed from provisioned --> autoscale
|
||||||
|
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||||
|
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||||
|
delete newOffer.content.offerAutopilotSettings;
|
||||||
|
} else {
|
||||||
|
delete newOffer.content.offerThroughput;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isAutoPilotSelected(false);
|
||||||
|
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
|
||||||
|
|
||||||
|
// user has changed from autoscale --> provisioned
|
||||||
|
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||||
|
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||||
|
} else {
|
||||||
|
delete newOffer.content.offerAutopilotSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
|
this.container != null
|
||||||
|
) {
|
||||||
|
const requestPayload = {
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
databaseName: this.collection.databaseId,
|
||||||
|
collectionName: this.collection.id(),
|
||||||
|
throughput: newThroughput,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||||
|
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||||
|
this.throughput(originalThroughputValue);
|
||||||
|
this.notificationStatusInfo(
|
||||||
|
throughputApplyDelayedMessage(
|
||||||
|
this.isAutoPilotSelected(),
|
||||||
|
originalThroughputValue,
|
||||||
|
this._getThroughputUnit(),
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id(),
|
||||||
|
newThroughput
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.throughput.valueHasMutated(); // force component re-render
|
||||||
|
} else {
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.collection.offer.valueHasMutated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.isRefreshingExplorer(false);
|
||||||
|
this._setBaseline();
|
||||||
|
this.collection.readSettings();
|
||||||
|
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||||
|
TelemetryProcessor.traceSuccess(
|
||||||
|
Action.UpdateSettings,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.tabTitle()
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.container.isRefreshingExplorer(false);
|
||||||
|
this.isExecutionError(true);
|
||||||
|
console.error(error);
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.UpdateSettings,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
databaseName: this.collection && this.collection.databaseId,
|
||||||
|
collectionName: this.collection && this.collection.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.tabTitle(),
|
||||||
|
error: error
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isExecuting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
public onRevertClick = (): Q.Promise<any> => {
|
public onRevertClick = (): Q.Promise<any> => {
|
||||||
|
|||||||
@@ -648,7 +648,9 @@ 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);
|
||||||
@@ -657,9 +659,7 @@ 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);
|
||||||
const isDatabaseShared = this.getDatabase() && this.getDatabase().isDatabaseShared();
|
if (!collectionOffer) {
|
||||||
const isServerless = this.container.isServerlessEnabled();
|
|
||||||
if ((isDatabaseShared || isServerless) && !collectionOffer) {
|
|
||||||
this.quotaInfo(quotaInfo);
|
this.quotaInfo(quotaInfo);
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadOffers,
|
Action.LoadOffers,
|
||||||
|
|||||||
@@ -123,10 +123,6 @@ export default class Database implements ViewModels.Database {
|
|||||||
|
|
||||||
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(),
|
||||||
@@ -138,7 +134,9 @@ 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);
|
||||||
@@ -147,6 +145,11 @@ 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:
|
||||||
|
|||||||
@@ -546,43 +546,52 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
(activeTab as any).notebookPath() === item.path
|
(activeTab as any).notebookPath() === item.path
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextMenu: createFileContextMenu
|
contextMenu: createFileContextMenu && this.createFileContextMenu(item),
|
||||||
? [
|
|
||||||
{
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -114,13 +114,17 @@ export default class TelemetryProcessor {
|
|||||||
return validTimestamp;
|
return validTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getData(data?: any): any {
|
private static getData(data: any = {}): any {
|
||||||
|
if (typeof data === "string") {
|
||||||
|
data = { message: data };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
|
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
|
||||||
authType: (window as any).authType,
|
authType: (window as any).authType,
|
||||||
subscriptionId: userContext.subscriptionId,
|
subscriptionId: userContext.subscriptionId,
|
||||||
platform: configContext.platform,
|
platform: configContext.platform,
|
||||||
...(data ? data : [])
|
env: process.env.NODE_ENV,
|
||||||
|
...data
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface UserContext {
|
|||||||
authorizationToken?: string;
|
authorizationToken?: string;
|
||||||
resourceToken?: string;
|
resourceToken?: string;
|
||||||
defaultExperience?: DefaultAccountExperienceType;
|
defaultExperience?: DefaultAccountExperienceType;
|
||||||
|
useSDKOperations?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContext: Readonly<UserContext> = {} as const;
|
const userContext: Readonly<UserContext> = {} as const;
|
||||||
|
|||||||
@@ -6,15 +6,9 @@ Instead, generate ARM clients that consume this function with stricter typing.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import promiseRetry, { AbortError } from "p-retry";
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
|
import { ErrorResponse } from "./generatedClients/2020-04-01/types";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
interface ErrorResponse {
|
|
||||||
error: {
|
|
||||||
code: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ARMError extends Error {
|
interface ARMError extends Error {
|
||||||
code: string;
|
code: string;
|
||||||
}
|
}
|
||||||
@@ -40,8 +34,8 @@ export async function armRequest<T>({ host, path, apiVersion, method, body: requ
|
|||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorResponse = (await response.json()) as ErrorResponse;
|
const errorResponse = (await response.json()) as ErrorResponse;
|
||||||
const error = new Error(errorResponse.error?.message) as ARMError;
|
const error = new Error(errorResponse.message) as ARMError;
|
||||||
error.code = errorResponse.error.code;
|
error.code = errorResponse.code;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +78,8 @@ async function getOperationStatus(operationStatusUrl: string) {
|
|||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorResponse = (await response.json()) as ErrorResponse;
|
const errorResponse = (await response.json()) as ErrorResponse;
|
||||||
const error = new Error(errorResponse.error?.message) as ARMError;
|
const error = new Error(errorResponse.message) as ARMError;
|
||||||
error.code = errorResponse.error.code;
|
error.code = errorResponse.code;
|
||||||
throw new AbortError(error);
|
throw new AbortError(error);
|
||||||
}
|
}
|
||||||
const body = (await response.json()) as OperationResponse;
|
const body = (await response.json()) as OperationResponse;
|
||||||
|
|||||||
Reference in New Issue
Block a user