mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-12 06:00:02 +00:00
Compare commits
3 Commits
users/srna
...
user/swvis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35b15a1e20 | ||
|
|
17fd2185dc | ||
|
|
a93c8509cd |
@@ -3,7 +3,11 @@ PORTAL_RUNNER_PASSWORD=
|
||||
PORTAL_RUNNER_SUBSCRIPTION=
|
||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||
PORTAL_RUNNER_CONNECTION_STRING=
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||
CASSANDRA_CONNECTION_STRING=
|
||||
MONGO_CONNECTION_STRING=
|
||||
TABLES_CONNECTION_STRING=
|
||||
|
||||
@@ -288,8 +288,6 @@ src/Utils/DatabaseAccountUtils.ts
|
||||
src/Utils/JunoUtils.ts
|
||||
src/Utils/MessageValidation.ts
|
||||
src/Utils/NotebookConfigurationUtils.ts
|
||||
src/Utils/OfferUtils.test.ts
|
||||
src/Utils/OfferUtils.ts
|
||||
src/Utils/PricingUtils.test.ts
|
||||
src/Utils/QueryUtils.test.ts
|
||||
src/Utils/QueryUtils.ts
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -146,6 +146,13 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
|
||||
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||
|
||||
1527
package-lock.json
generated
1527
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,15 +4,17 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.9.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.1.0",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
"@nteract/commutable": "7.3.2",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/core": "15.1.0",
|
||||
"@nteract/data-explorer": "8.0.3",
|
||||
"@nteract/data-explorer": "8.2.9",
|
||||
"@nteract/directory-listing": "2.0.6",
|
||||
"@nteract/dropdown-menu": "1.0.1",
|
||||
"@nteract/editor": "10.1.2",
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
import {
|
||||
ConflictDefinition,
|
||||
FeedOptions,
|
||||
ItemDefinition,
|
||||
OfferDefinition,
|
||||
QueryIterator,
|
||||
Resource
|
||||
} from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import Q from "q";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
import { OfferUtils } from "../Utils/OfferUtils";
|
||||
import * as Constants from "./Constants";
|
||||
import { client } from "./CosmosClient";
|
||||
import * as HeadersUtility from "./HeadersUtility";
|
||||
import { sendCachedDataMessage } from "./MessageHandler";
|
||||
|
||||
export function getCommonQueryOptions(options: FeedOptions): any {
|
||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||
|
||||
62
src/Common/OfferUtility.test.ts
Normal file
62
src/Common/OfferUtility.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as OfferUtility from "./OfferUtility";
|
||||
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
|
||||
describe("parseSDKOfferResponse", () => {
|
||||
it("manual throughput", () => {
|
||||
const mockOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: 500,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 400,
|
||||
numPhysicalPartitions: 1
|
||||
}
|
||||
},
|
||||
id: "test"
|
||||
} as SDKOfferDefinition;
|
||||
|
||||
const mockResponse = {
|
||||
resource: mockOfferDefinition
|
||||
} as OfferResponse;
|
||||
|
||||
const expectedResult: Offer = {
|
||||
manualThroughput: 500,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("autoscale throughput", () => {
|
||||
const mockOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: 400,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 400,
|
||||
numPhysicalPartitions: 1
|
||||
},
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 5000
|
||||
}
|
||||
},
|
||||
id: "test"
|
||||
} as SDKOfferDefinition;
|
||||
|
||||
const mockResponse = {
|
||||
resource: mockOfferDefinition
|
||||
} as OfferResponse;
|
||||
|
||||
const expectedResult: Offer = {
|
||||
manualThroughput: undefined,
|
||||
autoscaleMaxThroughput: 5000,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
33
src/Common/OfferUtility.ts
Normal file
33
src/Common/OfferUtility.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
|
||||
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||
const offerContent = offerDefinition.content;
|
||||
if (!offerContent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||
|
||||
if (autopilotSettings) {
|
||||
return {
|
||||
id: offerDefinition.id,
|
||||
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
headers: offerResponse.headers
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerDefinition.id,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: offerContent.offerThroughput,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
headers: offerResponse.headers
|
||||
};
|
||||
};
|
||||
@@ -1,9 +1,6 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
@@ -11,50 +8,22 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readCollectionOffer = async (
|
||||
params: DataModels.ReadCollectionOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||
let offerId = params.offerId;
|
||||
if (!offerId) {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
try {
|
||||
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
||||
} catch (error) {
|
||||
clearMessage();
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||
throw error;
|
||||
@@ -63,61 +32,90 @@ export const readCollectionOffer = async (
|
||||
}
|
||||
};
|
||||
|
||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
};
|
||||
const resource = rpResponse?.properties?.resource;
|
||||
if (resource) {
|
||||
const offerId: string = rpResponse.name;
|
||||
const minimumThroughput: number =
|
||||
typeof resource.minimumThroughput === "string"
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
|
||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
||||
return offer?.id;
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -1,51 +1,28 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readDatabaseOffer = async (
|
||||
params: DataModels.ReadDatabaseOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||
let offerId = params.offerId;
|
||||
if (!offerId) {
|
||||
offerId = await (window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readDatabaseOfferWithARM(params.databaseId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||
throw error;
|
||||
@@ -54,13 +31,13 @@ export const readDatabaseOffer = async (
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
@@ -78,18 +55,39 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||
return offer?.id;
|
||||
const resource = rpResponse?.properties?.resource;
|
||||
if (resource) {
|
||||
const offerId: string = rpResponse.name;
|
||||
const minimumThroughput: number =
|
||||
typeof resource.minimumThroughput === "string"
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readOffers } from "./readOffers";
|
||||
|
||||
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
||||
if (!offerId) {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === resourceId);
|
||||
|
||||
if (!offer) {
|
||||
return undefined;
|
||||
}
|
||||
offerId = offer.id;
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
|
||||
return parseSDKOfferResponse(response);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||
|
||||
export const readOffers = async (): Promise<Offer[]> => {
|
||||
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readCollectionOffer } from "./readCollectionOffer";
|
||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||
import {
|
||||
@@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
||||
};
|
||||
|
||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
const currentOffer = params.currentOffer;
|
||||
const newOffer: Offer = {
|
||||
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||
const newOffer: SDKOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: undefined,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
},
|
||||
_etag: undefined,
|
||||
_ts: undefined,
|
||||
_rid: currentOffer._rid,
|
||||
_self: currentOffer._self,
|
||||
id: currentOffer.id,
|
||||
offerResourceId: currentOffer.offerResourceId,
|
||||
offerVersion: currentOffer.offerVersion,
|
||||
offerType: currentOffer.offerType,
|
||||
resource: currentOffer.resource
|
||||
_rid: sdkOfferDefinition._rid,
|
||||
_self: sdkOfferDefinition._self,
|
||||
id: sdkOfferDefinition.id,
|
||||
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||
offerVersion: sdkOfferDefinition.offerVersion,
|
||||
offerType: sdkOfferDefinition.offerType,
|
||||
resource: sdkOfferDefinition.resource
|
||||
};
|
||||
|
||||
if (params.autopilotThroughput) {
|
||||
@@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
.offer(params.currentOffer.id)
|
||||
// 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);
|
||||
return sdkResponse?.resource;
|
||||
|
||||
return parseSDKOfferResponse(sdkResponse);
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
||||
|
||||
describe("updateOfferThroughputBeyondLimit", () => {
|
||||
it("should call fetch", async () => {
|
||||
window.fetch = jest.fn(() => {
|
||||
return {
|
||||
ok: true
|
||||
};
|
||||
});
|
||||
window.dataExplorer = {
|
||||
logConsoleData: jest.fn(),
|
||||
deleteInProgressConsoleDataWithId: jest.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
await updateOfferThroughputBeyondLimit({
|
||||
subscriptionId: "foo",
|
||||
resourceGroup: "foo",
|
||||
databaseAccountName: "foo",
|
||||
databaseName: "foo",
|
||||
throughput: 1000000000,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
});
|
||||
expect(window.fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Platform, configContext } from "../../ConfigContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
interface UpdateOfferThroughputRequest {
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
databaseAccountName: string;
|
||||
databaseName: string;
|
||||
collectionName?: string;
|
||||
throughput: number;
|
||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||
}
|
||||
|
||||
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
||||
if (configContext.platform !== Platform.Portal) {
|
||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
||||
}
|
||||
|
||||
const resourceDescriptionInfo = request.collectionName
|
||||
? `database ${request.databaseName} and container ${request.collectionName}`
|
||||
: `database ${request.databaseName}`;
|
||||
|
||||
const clearMessage = logConsoleProgress(
|
||||
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||
);
|
||||
|
||||
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logConsoleInfo(
|
||||
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||
);
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const error = await response.json();
|
||||
handleError(
|
||||
error,
|
||||
"updateOfferThroughputBeyondLimit",
|
||||
`Failed to request an increase in throughput for ${request.throughput}`
|
||||
);
|
||||
clearMessage();
|
||||
throw error;
|
||||
}
|
||||
@@ -208,12 +208,21 @@ export interface QueryMetrics {
|
||||
vmExecutionTime: any;
|
||||
}
|
||||
|
||||
export interface Offer extends Resource {
|
||||
export interface Offer {
|
||||
id: string;
|
||||
autoscaleMaxThroughput: number;
|
||||
manualThroughput: number;
|
||||
minimumThroughput: number;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
headers?: any;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
offerVersion?: string;
|
||||
offerType?: string;
|
||||
content?: {
|
||||
offerThroughput: number;
|
||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||
collectionThroughputInfo?: OfferThroughputInfo;
|
||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||
};
|
||||
@@ -221,10 +230,6 @@ export interface Offer extends Resource {
|
||||
offerResourceId?: string;
|
||||
}
|
||||
|
||||
export interface OfferWithHeaders extends Offer {
|
||||
headers: any;
|
||||
}
|
||||
|
||||
export interface CollectionQuotaInfo {
|
||||
storedProcedures: number;
|
||||
triggers: number;
|
||||
|
||||
@@ -32,7 +32,8 @@ export enum MessageTypes {
|
||||
GetArcadiaToken,
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount
|
||||
RefreshDatabaseAccount,
|
||||
InitTestExplorer
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -39,6 +39,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
||||
}
|
||||
|
||||
params.set("account","contoso-retail-mongodb");
|
||||
params.set("port","10255");
|
||||
//tofill
|
||||
params.set("token","");
|
||||
return params;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,12 +89,11 @@ describe("SettingsComponent", () => {
|
||||
it("auto pilot helper functions pass on correct value", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 10000
|
||||
}
|
||||
}
|
||||
} as DataModels.Offer);
|
||||
autoscaleMaxThroughput: 10000,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test"
|
||||
});
|
||||
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
@@ -187,21 +186,6 @@ describe("SettingsComponent", () => {
|
||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
||||
});
|
||||
|
||||
it("isOfferReplacePending", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable({
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
|
||||
settingsComponentInstance = new SettingsComponent(props);
|
||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
|
||||
});
|
||||
|
||||
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||
|
||||
@@ -2,28 +2,23 @@ import * as React from "react";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||
import { mongoIndexingPolicyAADError, throughputUnit } from "./SettingsRenderUtils";
|
||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||
import {
|
||||
MongoIndexingPolicyComponent,
|
||||
MongoIndexingPolicyComponentProps
|
||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||
import {
|
||||
getMaxRUs,
|
||||
hasDatabaseSharedThroughput,
|
||||
GeospatialConfigType,
|
||||
TtlType,
|
||||
@@ -275,19 +270,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
private setAutoPilotStates = (): void => {
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
||||
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||
|
||||
if (
|
||||
offerAutopilotSettings &&
|
||||
offerAutopilotSettings.maxThroughput &&
|
||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
||||
) {
|
||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
this.setState({
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -305,12 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
!!this.collection.conflictResolutionPolicy();
|
||||
|
||||
public isOfferReplacePending = (): boolean => {
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
return (
|
||||
offer &&
|
||||
Object.keys(offer).find(value => value === "headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
|
||||
};
|
||||
|
||||
public onSaveClick = async (): Promise<void> => {
|
||||
@@ -448,103 +433,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
const newThroughput = this.state.throughput;
|
||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
||||
const originalThroughputValue: number = this.state.throughput;
|
||||
|
||||
if (newOffer.content) {
|
||||
newOffer.content.offerThroughput = newThroughput;
|
||||
} else {
|
||||
newOffer.content = {
|
||||
offerThroughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
maxThroughput: this.state.autoPilotThroughput
|
||||
};
|
||||
|
||||
// user has changed from provisioned --> autoscale
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
} else {
|
||||
delete newOffer.content.offerThroughput;
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
isAutoPilotSelected: false
|
||||
});
|
||||
|
||||
// user has changed from autoscale --> provisioned
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||
} else {
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getMaxRUs(this.collection, this.container) <=
|
||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.container
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.setState({
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughput: originalThroughputValue,
|
||||
throughputBaseline: originalThroughputValue,
|
||||
initialNotification: {
|
||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
||||
} as DataModels.Notification
|
||||
});
|
||||
} else {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
this.setState({
|
||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
||||
});
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.content.offerThroughput,
|
||||
throughputBaseline: updatedOffer.content.offerThroughput
|
||||
});
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
this.setState({
|
||||
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.manualThroughput,
|
||||
throughputBaseline: updatedOffer.manualThroughput
|
||||
});
|
||||
}
|
||||
}
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.setBaseline();
|
||||
@@ -809,7 +725,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
||||
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||
? ChangeFeedPolicyState.On
|
||||
: ChangeFeedPolicyState.Off;
|
||||
|
||||
@@ -42,7 +42,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
{getToolTipContainer(<span>Sample Text</span>)}
|
||||
|
||||
@@ -319,14 +319,13 @@ export const getThroughputApplyShortDelayMessage = (
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
databaseName: string,
|
||||
collectionName: string,
|
||||
targetThroughput: number
|
||||
collectionName: string
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||
<br />
|
||||
Database: {databaseName}, Container: {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
|
||||
} as DataModels.Notification
|
||||
};
|
||||
|
||||
it("renders with correct intiial notification", () => {
|
||||
it("renders with correct initial notification", () => {
|
||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||
@@ -54,16 +54,13 @@ describe("ScaleComponent", () => {
|
||||
|
||||
const newCollection = { ...collection };
|
||||
const maxThroughput = 5000;
|
||||
const targetMaxThroughput = 50000;
|
||||
newCollection.offer = ko.observable({
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: maxThroughput,
|
||||
targetMaxThroughput: targetMaxThroughput
|
||||
}
|
||||
},
|
||||
manualThroughput: undefined,
|
||||
autoscaleMaxThroughput: maxThroughput,
|
||||
minimumThroughput: 400,
|
||||
id: "offer",
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
});
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
initialNotification: undefined as DataModels.Notification,
|
||||
@@ -73,7 +70,6 @@ describe("ScaleComponent", () => {
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
||||
});
|
||||
|
||||
it("autoScale disabled", () => {
|
||||
@@ -109,11 +105,6 @@ describe("ScaleComponent", () => {
|
||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||
});
|
||||
|
||||
it("getMaxRUThroughputInputLimit", () => {
|
||||
const scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
||||
});
|
||||
|
||||
it("getThroughputTitle", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
@@ -138,14 +129,8 @@ describe("ScaleComponent", () => {
|
||||
|
||||
it("getThroughputWarningMessage", () => {
|
||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
||||
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||
let scaleComponent = new ScaleComponent(newProps);
|
||||
const scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||
|
||||
newProps.throughput = throughputBeyondMaxRus;
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,10 +12,9 @@ import {
|
||||
throughputUnit,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage
|
||||
updateThroughputBeyondLimitWarningMessage
|
||||
} from "../SettingsRenderUtils";
|
||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
@@ -62,11 +61,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
};
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
const isFixed =
|
||||
!this.props.collection.partitionKey ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
||||
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
||||
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
@@ -75,12 +70,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
);
|
||||
};
|
||||
|
||||
public getMaxRUThroughputInputLimit = (): number => {
|
||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
public getMaxRUs = (): number => {
|
||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
}
|
||||
|
||||
return getMaxRUs(this.props.collection, this.props.container);
|
||||
if (this.props.isFixedContainer) {
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition;
|
||||
}
|
||||
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
};
|
||||
|
||||
public getMinRUs = (): number => {
|
||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
return (
|
||||
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||
);
|
||||
};
|
||||
|
||||
public getThroughputTitle = (): string => {
|
||||
@@ -88,11 +97,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||
}
|
||||
|
||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
const maxThroughput: string =
|
||||
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
||||
? "unlimited"
|
||||
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||
};
|
||||
|
||||
@@ -109,26 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return this.getLongDelayMessage();
|
||||
}
|
||||
|
||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
||||
if (
|
||||
offer &&
|
||||
Object.keys(offer).find(value => {
|
||||
return value === "headers";
|
||||
}) &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
) {
|
||||
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
|
||||
|
||||
const targetThroughput =
|
||||
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
|
||||
|
||||
const offer = this.props.collection?.offer();
|
||||
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
return getThroughputApplyShortDelayMessage(
|
||||
this.props.isAutoPilotSelected,
|
||||
throughput,
|
||||
throughputUnit,
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id(),
|
||||
targetThroughput
|
||||
this.props.collection.id()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,21 +133,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
public getThroughputWarningMessage = (): JSX.Element => {
|
||||
const throughputExceedsBackendLimits: boolean =
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
getMaxRUs(this.props.collection, this.props.container) <=
|
||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
|
||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||
return updateThroughputBeyondLimitWarningMessage;
|
||||
}
|
||||
|
||||
const throughputExceedsMaxValue: boolean =
|
||||
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
|
||||
|
||||
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||
return updateThroughputDelayedApplyWarningMessage;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -183,8 +169,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
throughput={this.props.throughput}
|
||||
throughputBaseline={this.props.throughputBaseline}
|
||||
onThroughputChange={this.props.onThroughputChange}
|
||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
||||
maximum={this.getMaxRUThroughputInputLimit()}
|
||||
minimum={this.getMinRUs()}
|
||||
maximum={this.getMaxRUs()}
|
||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||
label={this.getThroughputTitle()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
@@ -48,7 +48,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
label="Throughput (6,000 - unlimited RU/s)"
|
||||
maxAutoPilotThroughput={4000}
|
||||
maxAutoPilotThroughputBaseline={4000}
|
||||
maximum={40000}
|
||||
maximum={1000000}
|
||||
minimum={6000}
|
||||
onAutoPilotSelected={[Function]}
|
||||
onMaxAutoPilotThroughputChange={[Function]}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { collection, container } from "./TestUtils";
|
||||
import { collection } from "./TestUtils";
|
||||
import {
|
||||
getMaxRUs,
|
||||
getMinRUs,
|
||||
getMongoIndexType,
|
||||
getMongoNotification,
|
||||
getSanitizedInputValue,
|
||||
@@ -23,16 +21,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import ko from "knockout";
|
||||
|
||||
describe("SettingsUtils", () => {
|
||||
it("getMaxRUs", () => {
|
||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||
expect(getMaxRUs(collection, container)).toEqual(40000);
|
||||
});
|
||||
|
||||
it("getMinRUs", () => {
|
||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||
expect(getMinRUs(collection, container)).toEqual(6000);
|
||||
});
|
||||
|
||||
it("hasDatabaseSharedThroughput", () => {
|
||||
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
|
||||
import Explorer from "../../Explorer";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
@@ -71,57 +67,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
||||
return database?.isDatabaseShared() && !collection.offer();
|
||||
};
|
||||
|
||||
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
|
||||
if (isTryCosmosDBSubscription) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
}
|
||||
|
||||
const numPartitionsFromOffer: number =
|
||||
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
|
||||
|
||||
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
|
||||
|
||||
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
|
||||
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||
};
|
||||
|
||||
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
||||
if (isTryCosmosDBSubscription) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const offerContent = collection?.offer && collection.offer()?.content;
|
||||
|
||||
if (offerContent?.offerAutopilotSettings) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
|
||||
|
||||
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
|
||||
return collectionThroughputInfo.minimumRUForCollection;
|
||||
}
|
||||
|
||||
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
|
||||
|
||||
if (!numPartitions || numPartitions === 1) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
|
||||
const quotaInKb = collection.quotaInfo().collectionSize;
|
||||
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
||||
|
||||
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
||||
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
||||
|
||||
return Math.max(baseRU, baseRUbyPartitions);
|
||||
};
|
||||
|
||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||
// Backend can contain different casing as it does case-insensitive comparisson
|
||||
if (!modeFromBackend) {
|
||||
|
||||
@@ -20,15 +20,11 @@ export const collection = ({
|
||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
||||
offer: ko.observable<DataModels.Offer>({
|
||||
content: {
|
||||
offerThroughput: 10000,
|
||||
offerIsRUPerMinuteThroughputEnabled: false,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 6000,
|
||||
numPhysicalPartitions: 4
|
||||
} as DataModels.OfferThroughputInfo
|
||||
}
|
||||
} as DataModels.Offer),
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: 10000,
|
||||
minimumThroughput: 6000,
|
||||
id: "offer"
|
||||
}),
|
||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||
{} as DataModels.ConflictResolutionPolicy
|
||||
),
|
||||
|
||||
@@ -227,7 +227,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
, Container:
|
||||
sampleCollection
|
||||
|
||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||
, Current manual throughput: 1000 RU/s
|
||||
</Text>
|
||||
<Text
|
||||
id="throughputApplyLongDelayMessage"
|
||||
|
||||
@@ -1730,6 +1730,7 @@ export default class Explorer {
|
||||
case MessageTypes.SendNotification:
|
||||
case MessageTypes.ClearNotification:
|
||||
case MessageTypes.LoadingStatus:
|
||||
case MessageTypes.InitTestExplorer:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,9 @@ export const launchWebSocketKernelEpic = (
|
||||
} as any,
|
||||
name: "",
|
||||
path: content.filepath.replace(/^\/+/g, ""),
|
||||
type: "notebook"
|
||||
type: "notebook",
|
||||
endpoint:"https://dech-notebooks-demo-7.documents.azure.com:443/",
|
||||
token:"" //to fill
|
||||
};
|
||||
|
||||
return sessions.create(serverConfig, sessionPayload).pipe(
|
||||
|
||||
@@ -50,13 +50,24 @@
|
||||
id="fileImportLinkNotebook"
|
||||
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
||||
>
|
||||
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
|
||||
<img
|
||||
id="importFileButton"
|
||||
class="fileImportImg"
|
||||
src="/folder_16x16.svg"
|
||||
alt="upload files"
|
||||
title="Upload files"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paneFooter">
|
||||
<div class="leftpanel-okbut">
|
||||
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
|
||||
<input
|
||||
id="uploadFileButton"
|
||||
type="submit"
|
||||
data-bind="attr: { value: submitButtonLabel }"
|
||||
class="btncreatecoll1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload File inputs - End -->
|
||||
|
||||
@@ -16,8 +16,6 @@ import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../Explorer";
|
||||
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
@@ -35,11 +33,6 @@ const currentThroughput: (isAutoscale: boolean, throughput: number) => string =
|
||||
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
|
||||
: `Current manual throughput: ${throughput} RU/s`;
|
||||
|
||||
const throughputApplyDelayedMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||
`The request to increase the throughput has successfully been submitted.
|
||||
This operation will take 1-3 business days to complete. View the latest status in Notifications.<br />
|
||||
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`;
|
||||
|
||||
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||
`A request to increase the throughput is currently in progress.
|
||||
This operation will take some time to complete.<br />
|
||||
@@ -66,8 +59,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
public displayedError: ko.Observable<string>;
|
||||
public isTemplateReady: ko.Observable<boolean>;
|
||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||
public minRUs: ko.Computed<number>;
|
||||
public maxRUs: ko.Computed<number>;
|
||||
public minRUs: ko.Observable<number>;
|
||||
public maxRUs: ko.Observable<number>;
|
||||
public maxRUsText: ko.PureComputed<string>;
|
||||
public maxRUThroughputInputLimit: ko.Computed<number>;
|
||||
public notificationStatusInfo: ko.Observable<string>;
|
||||
@@ -92,7 +85,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
|
||||
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
||||
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
||||
private _offerReplacePending: ko.Computed<boolean>;
|
||||
private _offerReplacePending: ko.Observable<boolean>;
|
||||
private container: Explorer;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
@@ -111,15 +104,14 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
this._wasAutopilotOriginallySet = ko.observable(false);
|
||||
this.isAutoPilotSelected = editable.observable(false);
|
||||
this.autoPilotThroughput = editable.observable<number>();
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||
this.userCanChangeProvisioningTypes = ko.observable(true);
|
||||
|
||||
if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) {
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) {
|
||||
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
|
||||
if (autoscaleMaxThroughput) {
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
this._wasAutopilotOriginallySet(true);
|
||||
this.isAutoPilotSelected(true);
|
||||
this.autoPilotThroughput(offerAutopilotSettings.maxThroughput);
|
||||
this.autoPilotThroughput(autoscaleMaxThroughput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,45 +197,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
||||
});
|
||||
|
||||
this.minRUs = ko.computed<number>(() => {
|
||||
const offerContent =
|
||||
this.database && this.database.offer && this.database.offer() && this.database.offer().content;
|
||||
|
||||
// TODO: backend is returning 1,000,000 as min throughput which seems wrong
|
||||
// Setting to min throughput to not block and let the backend pass or fail
|
||||
if (offerContent && offerContent.offerAutopilotSettings) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||
offerContent && offerContent.collectionThroughputInfo;
|
||||
|
||||
if (collectionThroughputInfo && !!collectionThroughputInfo.minimumRUForCollection) {
|
||||
return collectionThroughputInfo.minimumRUForCollection;
|
||||
}
|
||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||
return throughputDefaults.unlimitedmin;
|
||||
});
|
||||
this.minRUs = ko.observable<number>(
|
||||
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
|
||||
);
|
||||
|
||||
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
||||
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
||||
});
|
||||
|
||||
this.maxRUs = ko.computed<number>(() => {
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||
this.database &&
|
||||
this.database.offer &&
|
||||
this.database.offer() &&
|
||||
this.database.offer().content &&
|
||||
this.database.offer().content.collectionThroughputInfo;
|
||||
const numPartitions = collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions;
|
||||
if (!!numPartitions) {
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||
}
|
||||
|
||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||
return throughputDefaults.unlimitedmax;
|
||||
});
|
||||
this.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
|
||||
|
||||
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
||||
if (configContext.platform === Platform.Hosted) {
|
||||
@@ -269,37 +231,23 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return this.throughputTitle() + this.requestUnitsUsageCost();
|
||||
});
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>();
|
||||
this._offerReplacePending = ko.pureComputed<boolean>(() => {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
return (
|
||||
offer &&
|
||||
offer.hasOwnProperty("headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
});
|
||||
this._offerReplacePending = ko.observable<boolean>(
|
||||
!!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
this.notificationStatusInfo = ko.observable<string>("");
|
||||
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
||||
this.warningMessage = ko.computed<string>(() => {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
|
||||
if (this.overrideWithProvisionedThroughputSettings()) {
|
||||
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
||||
}
|
||||
|
||||
if (
|
||||
offer &&
|
||||
offer.hasOwnProperty("headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
) {
|
||||
const throughput = offer.content.offerAutopilotSettings
|
||||
? offer.content.offerAutopilotSettings.maxThroughput
|
||||
: offer.content.offerThroughput;
|
||||
|
||||
const offer = this.database.offer();
|
||||
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
||||
}
|
||||
|
||||
if (
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.canThroughputExceedMaximumValue()
|
||||
) {
|
||||
@@ -432,60 +380,26 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
try {
|
||||
if (this.isAutoPilotSelected()) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: this.autoPilotThroughput(),
|
||||
manualThroughput: undefined,
|
||||
migrateToAutoPilot: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
||||
manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput()
|
||||
};
|
||||
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
} else {
|
||||
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
||||
const newThroughput = this.throughput();
|
||||
|
||||
if (
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.database.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.throughput(originalThroughputValue);
|
||||
this.notificationStatusInfo(
|
||||
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
} else {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: undefined,
|
||||
manualThroughput: newThroughput,
|
||||
migrateToManual: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
|
||||
const updatedOffer = await updateOffer(updateOfferParams);
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
}
|
||||
if (this._hasProvisioningTypeChanged()) {
|
||||
if (this.isAutoPilotSelected()) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._setBaseline();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
} catch (error) {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.isExecutionError(true);
|
||||
@@ -527,15 +441,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
|
||||
private _setBaseline() {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
const offerThroughput = offer.content && offer.content.offerThroughput;
|
||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||
|
||||
this.throughput.setBaseline(offerThroughput);
|
||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
|
||||
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
|
||||
this.throughput.setBaseline(offer.manualThroughput);
|
||||
this.userCanChangeProvisioningTypes(true);
|
||||
|
||||
const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput;
|
||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot));
|
||||
this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput);
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
|
||||
@@ -1295,8 +1295,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.databaseId,
|
||||
collectionName: this.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
offerVersion: this.offer()?.offerVersion
|
||||
defaultExperience: this.container.defaultExperience()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
|
||||
@@ -23,10 +23,14 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
|
||||
let headers: HeadersInit;
|
||||
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
|
||||
body = JSON.stringify({
|
||||
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]
|
||||
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint],
|
||||
account:"contoso-retail-mongodb",
|
||||
port: "10255",
|
||||
token:"" //tofill
|
||||
});
|
||||
headers = {
|
||||
[HttpHeaders.contentType]: "application/json"
|
||||
//"Access-Control-Allow-Origin": "https://localhost:5001"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,6 @@ export const minAutoPilotThroughput = 4000;
|
||||
|
||||
export const autoPilotIncrementStep = 1000;
|
||||
|
||||
export function isValidV3AutoPilotOffer(offer: Offer): boolean {
|
||||
const maxThroughput =
|
||||
offer &&
|
||||
offer.content &&
|
||||
offer.content.offerAutopilotSettings &&
|
||||
offer.content.offerAutopilotSettings.maxThroughput;
|
||||
return isValidAutoPilotThroughput(maxThroughput);
|
||||
}
|
||||
|
||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||
if (!maxThroughput) {
|
||||
return false;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import * as Constants from "../../src/Common/Constants";
|
||||
import * as DataModels from "../../src/Contracts/DataModels";
|
||||
import { OfferUtils } from "../../src/Utils/OfferUtils";
|
||||
|
||||
describe("OfferUtils tests", () => {
|
||||
const offerV1: DataModels.Offer = {
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_ts: 0,
|
||||
_etag: "",
|
||||
id: "v1",
|
||||
offerVersion: Constants.OfferVersions.V1,
|
||||
offerType: "Standard",
|
||||
offerResourceId: "",
|
||||
content: null,
|
||||
resource: ""
|
||||
};
|
||||
|
||||
const offerV2: DataModels.Offer = {
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_ts: 0,
|
||||
_etag: "",
|
||||
id: "v1",
|
||||
offerVersion: Constants.OfferVersions.V2,
|
||||
offerType: "Standard",
|
||||
offerResourceId: "",
|
||||
content: null,
|
||||
resource: ""
|
||||
};
|
||||
|
||||
describe("isOfferV1()", () => {
|
||||
it("should return true for V1", () => {
|
||||
expect(OfferUtils.isOfferV1(offerV1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false for V2", () => {
|
||||
expect(OfferUtils.isOfferV1(offerV2)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isNotOfferV1()", () => {
|
||||
it("should return true for V2", () => {
|
||||
expect(OfferUtils.isNotOfferV1(offerV2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false for V1", () => {
|
||||
expect(OfferUtils.isNotOfferV1(offerV1)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
|
||||
export class OfferUtils {
|
||||
public static isOfferV1(offer: DataModels.Offer): boolean {
|
||||
return !offer || offer.offerVersion !== Constants.OfferVersions.V2;
|
||||
}
|
||||
|
||||
public static isNotOfferV1(offer: DataModels.Offer): boolean {
|
||||
return !OfferUtils.isOfferV1(offer);
|
||||
}
|
||||
}
|
||||
@@ -82,10 +82,6 @@ describe("Collection Add and Delete Mongo spec", () => {
|
||||
);
|
||||
|
||||
if (collections.length) {
|
||||
await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, {
|
||||
visible: true
|
||||
});
|
||||
|
||||
const textId = await frame.evaluate(element => {
|
||||
return element.attributes["data-test"].textContent;
|
||||
}, collections[0]);
|
||||
|
||||
101
test/notebooks/notebookTestUtils.ts
Normal file
101
test/notebooks/notebookTestUtils.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ElementHandle, Frame } from "puppeteer";
|
||||
import { TestExplorerParams } from "./testExplorer/TestExplorerParams";
|
||||
import * as path from "path";
|
||||
|
||||
export const NOTEBOOK_OPERATION_DELAY = 5000;
|
||||
export const RENDER_DELAY = 2500;
|
||||
|
||||
let testExplorerFrame: Frame;
|
||||
export const getTestExplorerFrame = async (): Promise<Frame> => {
|
||||
if (testExplorerFrame) {
|
||||
return testExplorerFrame;
|
||||
}
|
||||
|
||||
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
|
||||
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
|
||||
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
|
||||
const portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
|
||||
const portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
|
||||
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
|
||||
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
|
||||
|
||||
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerTenantId,
|
||||
encodeURI(notebooksTestRunnerTenantId)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerClientId,
|
||||
encodeURI(notebooksTestRunnerClientId)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.notebooksTestRunnerClientSecret,
|
||||
encodeURI(notebooksTestRunnerClientSecret)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerDatabaseAccount,
|
||||
encodeURI(portalRunnerDatabaseAccount)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerDatabaseAccountKey,
|
||||
encodeURI(portalRunnerDatabaseAccountKey)
|
||||
);
|
||||
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
|
||||
testExplorerUrl.searchParams.append(
|
||||
TestExplorerParams.portalRunnerResourceGroup,
|
||||
encodeURI(portalRunnerResourceGroup)
|
||||
);
|
||||
|
||||
await page.goto(testExplorerUrl.toString());
|
||||
|
||||
const handle = await page.waitForSelector("iframe");
|
||||
testExplorerFrame = await handle.contentFrame();
|
||||
await testExplorerFrame.waitForSelector(".galleryHeader");
|
||||
return testExplorerFrame;
|
||||
};
|
||||
|
||||
export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: string): Promise<ElementHandle<Element>> => {
|
||||
const notebookNode = await getNotebookNode(frame, notebookName);
|
||||
if (notebookNode) {
|
||||
return notebookNode;
|
||||
}
|
||||
|
||||
const uploadNotebookPath = path.join(__dirname, "testNotebooks", notebookName);
|
||||
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||
const treeNodeHeadersBeforeUpload = await notebookResourceTree.$$(".treeNodeHeader");
|
||||
|
||||
const ellipses = await treeNodeHeadersBeforeUpload[2].$("button");
|
||||
await ellipses.click();
|
||||
|
||||
await frame.waitFor(RENDER_DELAY);
|
||||
|
||||
const menuItems = await frame.$$(".ms-ContextualMenu-item");
|
||||
await menuItems[4].click();
|
||||
|
||||
const uploadFileButton = await frame.waitForSelector("#importFileButton");
|
||||
uploadFileButton.click();
|
||||
|
||||
const fileChooser = await page.waitForFileChooser();
|
||||
fileChooser.accept([uploadNotebookPath]);
|
||||
|
||||
const submitButton = await frame.waitForSelector("#uploadFileButton");
|
||||
await submitButton.click();
|
||||
|
||||
await frame.waitFor(NOTEBOOK_OPERATION_DELAY);
|
||||
return await getNotebookNode(frame, notebookName);
|
||||
};
|
||||
|
||||
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
|
||||
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||
let currentNotebookNode: ElementHandle<Element>;
|
||||
|
||||
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");
|
||||
for (let i = 1; i < treeNodeHeaders.length; i++) {
|
||||
currentNotebookNode = treeNodeHeaders[i];
|
||||
const nodeLabel = await currentNotebookNode.$eval(".nodeLabel", element => element.textContent);
|
||||
if (nodeLabel === uploadNotebookName) {
|
||||
return currentNotebookNode;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
138
test/notebooks/testExplorer/TestExplorer.ts
Normal file
138
test/notebooks/testExplorer/TestExplorer.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { MessageTypes } from "../../../src/Contracts/ExplorerContracts";
|
||||
import "../../../less/hostedexplorer.less";
|
||||
import { TestExplorerParams } from "./TestExplorerParams";
|
||||
import { ClientSecretCredential } from "@azure/identity";
|
||||
import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models";
|
||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||
import * as msRest from "@azure/ms-rest-js";
|
||||
import * as ViewModels from "../../../src/Contracts/ViewModels";
|
||||
|
||||
class CustomSigner implements msRest.ServiceClientCredentials {
|
||||
private token: string;
|
||||
constructor(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
async signRequest(webResource: msRest.WebResourceLike): Promise<msRest.WebResourceLike> {
|
||||
webResource.headers.set("authorization", `bearer ${this.token}`);
|
||||
return webResource;
|
||||
}
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent): void => {
|
||||
if (event.data.type === MessageTypes.InitTestExplorer) {
|
||||
sendMessageToExplorerFrame(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
const AADLogin = async (
|
||||
notebooksTestRunnerApplicationId: string,
|
||||
notebooksTestRunnerClientId: string,
|
||||
notebooksTestRunnerClientSecret: string
|
||||
): Promise<string> => {
|
||||
const credentials = new ClientSecretCredential(
|
||||
notebooksTestRunnerApplicationId,
|
||||
notebooksTestRunnerClientId,
|
||||
notebooksTestRunnerClientSecret
|
||||
);
|
||||
const token = await credentials.getToken("https://management.core.windows.net/.default");
|
||||
return token.token;
|
||||
};
|
||||
|
||||
const getDatabaseAccount = async (
|
||||
token: string,
|
||||
notebooksAccountSubscriptonId: string,
|
||||
notebooksAccountResourceGroup: string,
|
||||
notebooksAccountName: string
|
||||
): Promise<DatabaseAccountsGetResponse> => {
|
||||
const client = new CosmosDBManagementClient(new CustomSigner(token), notebooksAccountSubscriptonId);
|
||||
return client.databaseAccounts.get(notebooksAccountResourceGroup, notebooksAccountName);
|
||||
};
|
||||
|
||||
const sendMessageToExplorerFrame = (data: unknown): void => {
|
||||
const explorerFrame = document.getElementById("explorerMenu") as HTMLIFrameElement;
|
||||
|
||||
explorerFrame &&
|
||||
explorerFrame.contentDocument &&
|
||||
explorerFrame.contentDocument.referrer &&
|
||||
explorerFrame.contentWindow.postMessage(
|
||||
{
|
||||
signature: "pcIframe",
|
||||
data: data
|
||||
},
|
||||
explorerFrame.contentDocument.referrer || window.location.href
|
||||
);
|
||||
};
|
||||
|
||||
const initTestExplorer = async (): Promise<void> => {
|
||||
window.addEventListener("message", handleMessage, false);
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const notebooksTestRunnerTenantId = decodeURIComponent(
|
||||
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerTenantId)
|
||||
);
|
||||
const notebooksTestRunnerClientId = decodeURIComponent(
|
||||
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientId)
|
||||
);
|
||||
const notebooksTestRunnerClientSecret = decodeURIComponent(
|
||||
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientSecret)
|
||||
);
|
||||
const portalRunnerDatabaseAccount = decodeURIComponent(
|
||||
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount)
|
||||
);
|
||||
const portalRunnerDatabaseAccountKey = decodeURIComponent(
|
||||
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccountKey)
|
||||
);
|
||||
const portalRunnerSubscripton = decodeURIComponent(urlSearchParams.get(TestExplorerParams.portalRunnerSubscripton));
|
||||
const portalRunnerResourceGroup = decodeURIComponent(
|
||||
urlSearchParams.get(TestExplorerParams.portalRunnerResourceGroup)
|
||||
);
|
||||
|
||||
const token = await AADLogin(
|
||||
notebooksTestRunnerTenantId,
|
||||
notebooksTestRunnerClientId,
|
||||
notebooksTestRunnerClientSecret
|
||||
);
|
||||
const databaseAccount = await getDatabaseAccount(
|
||||
token,
|
||||
portalRunnerSubscripton,
|
||||
portalRunnerResourceGroup,
|
||||
portalRunnerDatabaseAccount
|
||||
);
|
||||
|
||||
const initTestExplorerContent = {
|
||||
type: MessageTypes.InitTestExplorer,
|
||||
inputs: {
|
||||
databaseAccount: databaseAccount,
|
||||
subscriptionId: portalRunnerSubscripton,
|
||||
resourceGroup: portalRunnerResourceGroup,
|
||||
authorizationToken: `Bearer ${token}`,
|
||||
features: {},
|
||||
hasWriteAccess: true,
|
||||
csmEndpoint: "https://management.azure.com",
|
||||
dnsSuffix: "documents.azure.com",
|
||||
serverId: "prod1",
|
||||
extensionEndpoint: "/proxy",
|
||||
subscriptionType: 3,
|
||||
quotaId: "Internal_2014-09-01",
|
||||
addCollectionDefaultFlight: "2",
|
||||
isTryCosmosDBSubscription: false,
|
||||
masterKey: portalRunnerDatabaseAccountKey,
|
||||
loadDatabaseAccountTimestamp: 1604663109836,
|
||||
dataExplorerVersion: "1.0.1",
|
||||
sharedThroughputMinimum: 400,
|
||||
sharedThroughputMaximum: 1000000,
|
||||
sharedThroughputDefault: 400,
|
||||
defaultCollectionThroughput: {
|
||||
storage: "100",
|
||||
throughput: { fixed: 400, unlimited: 400, unlimitedmax: 100000, unlimitedmin: 400, shared: 400 }
|
||||
},
|
||||
// add UI test only when feature is not dependent on flights anymore
|
||||
flights: []
|
||||
} as ViewModels.DataExplorerInputsFrame
|
||||
};
|
||||
|
||||
window.postMessage(initTestExplorerContent, window.location.href);
|
||||
};
|
||||
|
||||
window.addEventListener("load", initTestExplorer);
|
||||
9
test/notebooks/testExplorer/TestExplorerParams.ts
Normal file
9
test/notebooks/testExplorer/TestExplorerParams.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum TestExplorerParams {
|
||||
notebooksTestRunnerTenantId = "notebooksTestRunnerTenantId",
|
||||
notebooksTestRunnerClientId = "notebooksTestRunnerClientId",
|
||||
notebooksTestRunnerClientSecret = "notebooksTestRunnerClientSecret",
|
||||
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
|
||||
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
|
||||
portalRunnerSubscripton = "portalRunnerSubscripton",
|
||||
portalRunnerResourceGroup = "portalRunnerResourceGroup"
|
||||
}
|
||||
18
test/notebooks/testExplorer/testExplorer.html
Normal file
18
test/notebooks/testExplorer/testExplorer.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
||||
<title>Azure Cosmos DB</title>
|
||||
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<iframe
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
class="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Portal"
|
||||
></iframe>
|
||||
</body>
|
||||
</html>
|
||||
110
test/notebooks/testNotebooks/GettingStarted.ipynb
Normal file
110
test/notebooks/testNotebooks/GettingStarted.ipynb
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "# Getting started with Cosmos notebooks\nIn this notebook, we'll learn how to use Cosmos notebook features. We'll create a database and container, import some sample data in a container in Azure Cosmos DB and run some queries over it."
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "### Create new database and container\n\nTo connect to the service, you can use our built-in instance of ```cosmos_client```. This is a ready to use instance of [CosmosClient](https://docs.microsoft.com/python/api/azure-cosmos/azure.cosmos.cosmos_client.cosmosclient?view=azure-python) from our Python SDK. It already has the context of this account baked in. We'll use ```cosmos_client``` to create a new database called **RetailDemo** and container called **WebsiteData**.\n\nOur dataset will contain events that occurred on the website - e.g. a user viewing an item, adding it to their cart, or purchasing it. We will partition by CartId, which represents the individual cart of each user. This will give us an even distribution of throughput and storage in our container. Learn more about how to [choose a good partition key.](https://docs.microsoft.com/azure/cosmos-db/partition-data)"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"trusted": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "import azure.cosmos\nfrom azure.cosmos.partition_key import PartitionKey\n\ndatabase = cosmos_client.create_database_if_not_exists('RetailDemo')\nprint('Database RetailDemo created')\n\ncontainer = database.create_container_if_not_exists(id='WebsiteData', partition_key=PartitionKey(path='/CartID'))\nprint('Container WebsiteData created')\n"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "#### Set the default database and container context to the new resources\n\nWe can use the ```%database {database_id}``` and ```%container {container_id}``` syntax."
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"trusted": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "%database RetailDemo"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"trusted": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "%container WebsiteData"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "### Load in sample JSON data and insert into the container. \nWe'll use the **%%upload** magic function to insert items into the container"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false,
|
||||
"inputHidden": false,
|
||||
"outputHidden": false,
|
||||
"trusted": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "%%upload --databaseName RetailDemo --containerName WebsiteData --url https://cosmosnotebooksdata.blob.core.windows.net/notebookdata/websiteData-small.json"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "The new database and container should show up under the **Data** section. Use the refresh icon after completing the previous cell. \n\n<img src=\"https://cosmosnotebooksdata.blob.core.windows.net/notebookdata/refreshData.png\" alt=\"Refresh Data resource tree to see newly created resources\" width=\"40%\"/>"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "### Run a query using the built-in Azure Cosmos notebook magic\n"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"trusted": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "%%sql\nSELECT c.Action, c.Price as ItemRevenue, c.Country, c.Item FROM c"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "We can get more information about the %%sql command using ```%%sql?```"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "### Next steps\n\nNow that you've learned how to use basic notebook functionality, follow the **Visualization.ipynb** notebook to further analyze and visualize our data. You can find it under the **Sample Notebooks** section."
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"version": "3.6.8"
|
||||
},
|
||||
"nteract": {
|
||||
"version": "dataExplorer 1.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
29
test/notebooks/uploadAndOpenNotebook.spec.ts
Normal file
29
test/notebooks/uploadAndOpenNotebook.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import "expect-puppeteer";
|
||||
import { getTestExplorerFrame, uploadNotebookIfNotExist } from "./notebookTestUtils";
|
||||
import { ElementHandle, Frame } from "puppeteer";
|
||||
|
||||
jest.setTimeout(300000);
|
||||
|
||||
const notebookName = "GettingStarted.ipynb";
|
||||
let frame: Frame;
|
||||
let uploadedNotebookNode: ElementHandle<Element>;
|
||||
|
||||
describe("Notebook UI tests", () => {
|
||||
it("Upload, Open and Delete Notebook", async () => {
|
||||
try {
|
||||
frame = await getTestExplorerFrame();
|
||||
uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName);
|
||||
await uploadedNotebookNode.click();
|
||||
await frame.waitForSelector(".tabNavText");
|
||||
const tabTitle = await frame.$eval(".tabNavText", element => element.textContent);
|
||||
expect(tabTitle).toEqual(notebookName);
|
||||
const closeIcon = await frame.waitForSelector(".close-Icon");
|
||||
await closeIcon.click();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const testName = (expect as any).getState().currentTestName;
|
||||
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,6 @@
|
||||
"noEmit": true,
|
||||
"types": ["jest"]
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"include": ["./src/**/*", "./test/notebooks/testExplorer/**/*"],
|
||||
"exclude": ["./src/**/__mocks__/**/*"]
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
"./src/Utils/BlobUtils.ts",
|
||||
"./src/Utils/GitHubUtils.ts",
|
||||
"./src/Utils/MessageValidation.ts",
|
||||
"./src/Utils/OfferUtils.ts",
|
||||
"./src/Utils/StringUtils.ts",
|
||||
"./src/Utils/WindowUtils.ts",
|
||||
"./src/Utils/arm/generatedClients/2020-04-01/types.ts",
|
||||
|
||||
@@ -92,7 +92,7 @@ module.exports = function(env = {}, argv = {}) {
|
||||
const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule];
|
||||
const envVars = {
|
||||
GIT_SHA: gitSha,
|
||||
PORT: process.env.PORT || "1234"
|
||||
PORT: process.env.PORT || "2223"
|
||||
};
|
||||
|
||||
if (mode === "production") {
|
||||
@@ -140,6 +140,11 @@ module.exports = function(env = {}, argv = {}) {
|
||||
template: "src/hostedExplorer.html",
|
||||
chunks: ["hostedExplorer"]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "testExplorer.html",
|
||||
template: "test/notebooks/testExplorer/testExplorer.html",
|
||||
chunks: ["testExplorer"]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "Heatmap.html",
|
||||
template: "src/Controls/Heatmap/Heatmap.html",
|
||||
@@ -178,6 +183,7 @@ module.exports = function(env = {}, argv = {}) {
|
||||
index: "./src/Index.ts",
|
||||
quickstart: "./src/quickstart.ts",
|
||||
hostedExplorer: "./src/HostedExplorer.ts",
|
||||
testExplorer: "./test/notebooks/testExplorer/TestExplorer.ts",
|
||||
heatmap: "./src/Controls/Heatmap/Heatmap.ts",
|
||||
terminal: "./src/Terminal/index.ts",
|
||||
notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",
|
||||
|
||||
Reference in New Issue
Block a user