Compare commits

..

8 Commits

Author SHA1 Message Date
Laurent Nguyen
77f895d343 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-28 11:19:33 +02:00
Laurent Nguyen
3bc2701356 Format and fix type issues 2020-08-27 16:43:39 +02:00
Laurent Nguyen
35dbaeea96 Add code completion implementation 2020-08-27 15:52:38 +02:00
Laurent Nguyen
18745a9ae6 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-27 09:13:52 +02:00
Laurent Nguyen
5be6f982f9 Resolve merge conflict 2020-08-13 16:35:26 +02:00
Laurent Nguyen
4fc9393b76 Merge branch 'master' into replace-codemirror-with-monaco 2020-08-13 16:30:46 +02:00
Laurent Nguyen
ee51e873b8 Fix build issues 2020-07-13 13:52:23 +02:00
Laurent Nguyen
206a8ef93b New Monaco Editor 2020-07-13 13:52:05 +02:00
28 changed files with 1401 additions and 651 deletions

View File

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

View File

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

View File

@@ -8,12 +8,15 @@ import {
ConflictDefinition,
FeedOptions,
ItemDefinition,
PartitionKeyDefinition,
QueryIterator,
Resource,
TriggerDefinition,
OfferDefinition
} from "@azure/cosmos";
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
import { client } from "./CosmosClient";
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { sendCachedDataMessage } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
@@ -477,6 +480,69 @@ export function readOffer(requestedResource: DataModels.Offer, options: any): Q.
);
}
export function getOrCreateDatabaseAndCollection(
request: DataModels.CreateDatabaseAndCollectionRequest,
options: any
): Q.Promise<DataModels.Collection> {
const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput");
const {
databaseId,
databaseLevelThroughput,
collectionId,
partitionKey,
indexingPolicy,
uniqueKeyPolicy,
offerThroughput,
analyticalStorageTtl,
hasAutoPilotV2FeatureFlag
} = request;
const createBody: DatabaseRequest = {
id: databaseId
};
// TODO: replace when SDK support autopilot
const initialHeaders = request.autoPilot
? !hasAutoPilotV2FeatureFlag
? {
[Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({
maxThroughput: request.autoPilot.maxThroughput
})
}
: {
[Constants.HttpHeaders.autoPilotTier]: request.autoPilot.autopilotTier
}
: undefined;
if (databaseLevelThroughput) {
if (request.autoPilot) {
databaseOptions.initialHeaders = initialHeaders;
}
createBody.throughput = offerThroughput;
}
return Q(
client()
.databases.createIfNotExists(createBody, databaseOptions)
.then(response => {
return response.database.containers.create(
{
id: collectionId,
partitionKey: (partitionKey || undefined) as PartitionKeyDefinition,
indexingPolicy: indexingPolicy ? indexingPolicy : undefined,
uniqueKeyPolicy: uniqueKeyPolicy ? uniqueKeyPolicy : undefined,
analyticalStorageTtl: analyticalStorageTtl,
throughput: databaseLevelThroughput || request.autoPilot ? undefined : offerThroughput
} as ContainerRequest, // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
{
initialHeaders: databaseLevelThroughput ? undefined : initialHeaders
}
);
})
.then(containerResponse => containerResponse.resource as DataModels.Collection)
.finally(() => refreshCachedResources(options))
);
}
export function refreshCachedOffers(): Q.Promise<void> {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,10 +3,8 @@ 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 {
CassandraKeyspaceCreateUpdateParameters,
GremlinDatabaseCreateUpdateParameters,
MongoDBDatabaseCreateUpdateParameters,
SqlDatabaseCreateUpdateParameters,
CreateUpdateOptions
} from "../../Utils/arm/generatedClients/2020-04-01/types";
@@ -81,7 +79,7 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
@@ -117,7 +115,7 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
@@ -127,7 +125,7 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
@@ -163,7 +161,7 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
@@ -189,7 +187,7 @@ async function createGremlineDatabase(params: DataModels.CreateDatabaseParams):
userContext.databaseAccount.name,
params.databaseId
);
if (getResponse?.properties?.resource) {
if (getResponse && getResponse.properties && getResponse.properties.resource) {
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
}
} catch (error) {
@@ -199,7 +197,7 @@ async function createGremlineDatabase(params: DataModels.CreateDatabaseParams):
}
const options: CreateUpdateOptions = constructRpOptions(params);
const rpPayload: GremlinDatabaseCreateUpdateParameters = {
const rpPayload: SqlDatabaseCreateUpdateParameters = {
properties: {
resource: {
id: params.databaseId
@@ -219,7 +217,8 @@ async function createGremlineDatabase(params: DataModels.CreateDatabaseParams):
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;
@@ -228,7 +227,7 @@ async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): P
}
}
const response: DatabaseResponse = await client().databases.create(createBody);
const response: DatabaseResponse = await client().databases.create(createBody, databaseOptions);
return response.resource;
}

View File

@@ -334,19 +334,6 @@ export interface CreateDatabaseParams {
offerThroughput?: number;
}
export interface CreateCollectionParams {
createNewDatabase: boolean;
collectionId: string;
databaseId: string;
databaseLevelThroughput: boolean;
offerThroughput: number;
analyticalStorageTtl?: number;
autoPilotMaxThroughput?: number;
indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey;
uniqueKeyPolicy?: UniqueKeyPolicy;
}
export interface SharedThroughputRange {
minimumRU: number;
maximumRU: number;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -852,10 +852,6 @@ export interface SqlContainerResource {
/* The conflict resolution policy for the container. */
conflictResolutionPolicy?: ConflictResolutionPolicy;
//TODO: this property is manually added. It should be auto-generated instead. Need to be fixed in the API spec.
/* Analytical storage time to live */
analyticalStorageTtl?: number;
}
/* Cosmos DB indexing policy */

View File

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

View File

@@ -24,15 +24,11 @@ interface Options {
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
export async function armRequest<T>({ host, path, apiVersion, method, body: requestBody }: Options): Promise<T> {
const url = new URL(path, host);
const authHeader = userContext.authorizationToken;
if (!authHeader) {
throw new Error("No ARM authorization header provided");
}
url.searchParams.append("api-version", apiVersion);
const response = await window.fetch(url.href, {
method,
headers: {
Authorization: authHeader
Authorization: userContext.authorizationToken
},
body: requestBody ? JSON.stringify(requestBody) : undefined
});
@@ -75,13 +71,9 @@ interface OperationResponse {
}
async function getOperationStatus(operationStatusUrl: string) {
const authHeader = userContext.authorizationToken;
if (!authHeader) {
throw new Error("No ARM authorization header provided");
}
const response = await window.fetch(operationStatusUrl, {
headers: {
Authorization: authHeader
Authorization: userContext.authorizationToken
}
});
if (!response.ok) {

View File

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

View File

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