Compare commits

..

4 Commits

Author SHA1 Message Date
Srinath Narayanan
9e8793d1b1 Moved constants 2020-11-11 11:31:07 -08:00
Srinath Narayanan
f41de718a0 fixed formatting error. 2020-11-11 08:58:43 -08:00
Srinath Narayanan
5820afc25a changed error message 2020-11-11 08:52:07 -08:00
Srinath Narayanan
6d94f18f9a Added loadOfffer with retry 2020-11-11 08:46:28 -08:00
28 changed files with 904 additions and 138 deletions

View File

@@ -0,0 +1,45 @@
import * as DataModels from "../../Contracts/DataModels";
import * as HeadersUtility from "../HeadersUtility";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerDefinition, Resource } from "@azure/cosmos";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
interface ResourceWithStatistics {
statistics: DataModels.Statistic[];
}
export const readCollectionQuotaInfo = async (
collection: ViewModels.Collection
): Promise<DataModels.CollectionQuotaInfo> => {
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
const options: RequestOptions = {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.read(options);
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
quota["usageSizeInKB"] = resource.statistics.reduce(
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
0
);
quota["numPartitions"] = resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
} catch (error) {
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -0,0 +1,25 @@
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();
});
});

View File

@@ -0,0 +1,57 @@
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;
}

View File

@@ -191,6 +191,18 @@ export interface OfferWithHeaders extends Offer {
headers: any;
}
export interface CollectionQuotaInfo {
storedProcedures: number;
triggers: number;
functions: number;
documentsSize: number;
collectionSize: number;
documentsCount: number;
usageSizeInKB: number;
numPartitions: number;
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
}
export interface OfferThroughputInfo {
minimumRUForCollection: number;
numPhysicalPartitions: number;

View File

@@ -117,6 +117,7 @@ export interface Collection extends CollectionBase {
analyticalStorageTtl: ko.Observable<number>;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
offer: ko.Observable<DataModels.Offer>;
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
@@ -161,6 +162,7 @@ export interface Collection extends CollectionBase {
loadStoredProcedures(): Promise<any>;
loadTriggers(): Promise<any>;
loadOffer(): Promise<void>;
loadAutopilotOfferWithRetry(): Promise<DataModels.Offer>;
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;

View File

@@ -1,49 +1,54 @@
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "office-ui-fabric-react";
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 * as Constants from "../../../Common/Constants";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
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 "./SettingsComponent.less";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { throughputUnit } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import {
MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import {
AddMongoIndexProps,
ChangeFeedPolicyState,
GeospatialConfigType,
getMongoNotification,
getTabTitle,
getMaxRUs,
hasDatabaseSharedThroughput,
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
SettingsV2TabTypes,
getTabTitle,
isDirty,
AddMongoIndexProps,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
SettingsV2TabTypes,
TtlType
getMongoNotification
} from "./SettingsUtils";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
import "./SettingsComponent.less";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
@@ -445,6 +450,7 @@ 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;
@@ -482,33 +488,65 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
}
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 });
if (this.state.isAutoPilotSelected) {
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({
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
isScaleSaveable: false,
isScaleDiscardable: false,
throughput: originalThroughputValue,
throughputBaseline: originalThroughputValue,
initialNotification: {
description: `Throughput update for ${newThroughput} ${throughputUnit}`
} as DataModels.Notification
});
} else {
this.setState({
throughput: updatedOffer.content.offerThroughput,
throughputBaseline: updatedOffer.content.offerThroughput
});
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);
if (this.state.isAutoPilotSelected) {
const autoPilotOffer = await this.collection.loadAutopilotOfferWithRetry();
this.setState({
autoPilotThroughput: autoPilotOffer.content.offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: autoPilotOffer.content.offerAutopilotSettings.maxThroughput
});
} else {
this.setState({
throughput: updatedOffer.content.offerThroughput,
throughputBaseline: updatedOffer.content.offerThroughput
});
}
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
}
}
this.container.isRefreshingExplorer(false);

View File

@@ -1,13 +1,14 @@
import { shallow } from "enzyme";
import ko from "knockout";
import React from "react";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { container, collection } from "../TestUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import Explorer from "../../../Explorer";
import * as Constants from "../../../../Common/Constants";
import * as DataModels from "../../../../Contracts/DataModels";
import Explorer from "../../../Explorer";
import { throughputUnit } from "../SettingsRenderUtils";
import { collection, container } from "../TestUtils";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import * as SharedConstants from "../../../../Shared/Constants";
import ko from "knockout";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer();
@@ -47,7 +48,9 @@ describe("ScaleComponent", () => {
let wrapper = shallow(<ScaleComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
const newCollection = { ...collection };
const maxThroughput = 5000;
@@ -106,6 +109,11 @@ 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)");
@@ -118,4 +126,26 @@ describe("ScaleComponent", () => {
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
});
it("canThroughputExceedMaximumValue", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
const newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
});
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);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
newProps.throughput = throughputBeyondMaxRus;
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
});
});

View File

@@ -1,20 +1,24 @@
import { Label, MessageBar, MessageBarType, Stack, Text, TextField } from "office-ui-fabric-react";
import * as React from "react";
import * as Constants from "../../../../Common/Constants";
import { configContext, Platform } from "../../../../ConfigContext";
import * as DataModels from "../../../../Contracts/DataModels";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import * as DataModels from "../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../Shared/Constants";
import Explorer from "../../../Explorer";
import {
getTextFieldStyles,
getThroughputApplyShortDelayMessage,
subComponentStackProps,
titleAndInputStackProps,
throughputUnit,
titleAndInputStackProps
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage,
updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage
} from "../SettingsRenderUtils";
import { getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import { getMaxRUs, getMinRUs, 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";
export interface ScaleComponentProps {
collection: ViewModels.Collection;
@@ -71,17 +75,40 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
);
};
public getMaxRUThroughputInputLimit = (): number => {
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
return getMaxRUs(this.props.collection, this.props.container);
};
public getThroughputTitle = (): string => {
if (this.props.isAutoPilotSelected) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : "10000";
const maxThroughput: string =
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
? "unlimited"
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
};
public canThroughputExceedMaximumValue = (): boolean => {
return (
!this.props.isFixedContainer &&
configContext.platform === Platform.Portal &&
!this.props.container.isRunningOnNationalCloud()
);
};
public getInitialNotificationElement = (): JSX.Element => {
if (this.props.initialNotification) {
return this.getLongDelayMessage();
}
const offer = this.props.collection?.offer && this.props.collection.offer();
if (
offer &&
@@ -108,6 +135,47 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return undefined;
};
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;
};
public getLongDelayMessage = (): JSX.Element => {
const matches: string[] = this.props.initialNotification?.description.match(
`Throughput update for (.*) ${throughputUnit}`
);
const throughput = this.props.throughputBaseline;
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
if (targetThroughput) {
return getThroughputApplyLongDelayMessage(
this.props.wasAutopilotOriginallySet,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
);
}
return <></>;
};
private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component
databaseAccount={this.props.container.databaseAccount()}
@@ -116,7 +184,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange}
minimum={getMinRUs(this.props.collection, this.props.container)}
maximum={this.getMaxRUThroughputInputLimit()}
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
label={this.getThroughputTitle()}
isEmulator={this.isEmulator}
isFixed={this.props.isFixedContainer}
@@ -129,6 +199,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
spendAckChecked={false}
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
/>
);

View File

@@ -15,6 +15,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
throughputBaseline: 100,
onThroughputChange: undefined,
minimum: 10000,
maximum: 400,
step: 100,
isEnabled: true,
isEmulator: false,
@@ -37,7 +38,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
},
onScaleDiscardableChange: () => {
return;
}
},
getThroughputWarningMessage: () => undefined
};
it("throughput input visible", () => {

View File

@@ -1,33 +1,35 @@
import React from "react";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import {
Checkbox,
getTextFieldStyles,
getToolTipContainer,
noLeftPaddingCheckBoxStyle,
titleAndInputStackProps,
checkBoxAndInputStackProps,
getChoiceGroupStyles,
messageBarStyles,
getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement
} from "../../SettingsRenderUtils";
import {
Text,
TextField,
ChoiceGroup,
IChoiceGroupOption,
Checkbox,
Stack,
Label,
Link,
MessageBar,
MessageBarType,
Stack,
Text,
TextField
MessageBarType
} from "office-ui-fabric-react";
import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import {
checkBoxAndInputStackProps,
getAutoPilotV3SpendElement,
getChoiceGroupStyles,
getEstimatedAutoscaleSpendElement,
getEstimatedSpendElement,
getTextFieldStyles,
getToolTipContainer,
manualToAutoscaleDisclaimerElement,
messageBarStyles,
noLeftPaddingCheckBoxStyle,
titleAndInputStackProps
} from "../../SettingsRenderUtils";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import * as SharedConstants from "../../../../../Shared/Constants";
import * as DataModels from "../../../../../Contracts/DataModels";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount;
@@ -36,6 +38,7 @@ export interface ThroughputInputAutoPilotV3Props {
throughputBaseline: number;
onThroughputChange: (newThroughput: number) => void;
minimum: number;
maximum: number;
step?: number;
isEnabled?: boolean;
spendAckChecked?: boolean;
@@ -56,6 +59,7 @@ export interface ThroughputInputAutoPilotV3Props {
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
}
interface ThroughputInputAutoPilotV3State {
@@ -115,7 +119,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
isDiscardable = true;
isSaveable = true;
if (!this.props.throughput || this.props.throughput < this.props.minimum) {
if (
!this.props.throughput ||
this.props.throughput < this.props.minimum ||
(this.props.throughput > this.props.maximum && (this.props.isEmulator || this.props.isFixed)) ||
(this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
!this.props.canExceedMaximumValue)
) {
isSaveable = false;
}
}
@@ -131,8 +141,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
};
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
this.throughputInputMaxValue = Number.MAX_SAFE_INTEGER;
this.autoPilotInputMaxValue = Number.MAX_SAFE_INTEGER;
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
this.autoPilotInputMaxValue = this.props.isFixed ? this.props.maximum : Int32.Max;
}
public hasProvisioningTypeChanged = (): boolean =>
@@ -296,6 +306,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onThroughputChange}
/>
{this.props.getThroughputWarningMessage() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{this.props.getThroughputWarningMessage()}
</MessageBar>
)}
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
{this.props.spendAckVisible && (

View File

@@ -8,6 +8,29 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
}
}
>
<StyledMessageBarBase
messageBarType={5}
>
<Text
id="throughputApplyLongDelayMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
test
, Container:
test
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
</Text>
</StyledMessageBarBase>
<Stack
tokens={
Object {
@@ -16,6 +39,8 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
}
>
<ThroughputInputAutoPilotV3Component
canExceedMaximumValue={true}
getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false}
isEmulator={false}
isEnabled={true}
@@ -23,6 +48,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
label="Throughput (6,000 - unlimited RU/s)"
maxAutoPilotThroughput={4000}
maxAutoPilotThroughputBaseline={4000}
maximum={40000}
minimum={6000}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}

View File

@@ -1,5 +1,6 @@
import { collection, container } from "./TestUtils";
import {
getMaxRUs,
getMinRUs,
getMongoIndexType,
getMongoNotification,
@@ -22,6 +23,11 @@ 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);

View File

@@ -1,9 +1,11 @@
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
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 { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import * as PricingUtils from "../../../Utils/PricingUtils";
import Explorer from "../../Explorer";
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
const zeroValue = 0;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
@@ -69,6 +71,22 @@ 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) {
@@ -87,7 +105,21 @@ export const getMinRUs = (collection: ViewModels.Collection, container: Explorer
return collectionThroughputInfo.minimumRUForCollection;
}
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
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 => {

View File

@@ -18,6 +18,7 @@ export const collection = ({
excludedPaths: []
}),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
offer: ko.observable<DataModels.Offer>({
content: {
offerThroughput: 10000,

View File

@@ -43,6 +43,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -85,6 +86,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -347,6 +349,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -572,6 +575,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -653,6 +657,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -755,6 +760,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -1295,6 +1301,7 @@ exports[`SettingsComponent renders 1`] = `
"version": 2,
},
"partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
}
@@ -1316,6 +1323,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -1358,6 +1366,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -1620,6 +1629,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -1845,6 +1855,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -1926,6 +1937,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -2028,6 +2040,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -2603,6 +2616,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -2645,6 +2659,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -2907,6 +2922,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -3132,6 +3148,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -3213,6 +3230,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -3315,6 +3333,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -3855,6 +3874,7 @@ exports[`SettingsComponent renders 1`] = `
"version": 2,
},
"partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function],
"uniqueKeyPolicy": Object {},
}
@@ -3876,6 +3896,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -3918,6 +3939,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -4180,6 +4202,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
@@ -4405,6 +4428,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"collectionId": [Function],
"collectionIdTitle": [Function],
"collectionWithThroughputInShared": [Function],
@@ -4486,6 +4510,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
@@ -4588,6 +4613,7 @@ exports[`SettingsComponent renders 1`] = `
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],

View File

@@ -61,6 +61,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
public maxCollectionsReachedMessage: ko.Observable<string>;
public requestUnitsUsageCost: ko.Computed<string>;
public dedicatedRequestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public largePartitionKey: ko.Observable<boolean> = ko.observable<boolean>(false);
public useIndexingForSharedThroughput: ko.Observable<boolean> = ko.observable<boolean>(true);
public costsVisible: ko.PureComputed<boolean>;
@@ -313,6 +314,19 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
});
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = this._getThroughput();
return offerThroughput <= 100000;
}
return false;
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
});

View File

@@ -117,6 +117,10 @@
showAutoPilot: !isFreeTierAccount()
}">
</throughput-input-autopilot-v3>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade --><a
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">Contact
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->

View File

@@ -31,6 +31,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
public throughputSpendAck: ko.Observable<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public requestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public costsVisible: ko.PureComputed<boolean>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
@@ -167,6 +168,19 @@ export default class AddDatabasePane extends ContextualPaneBase {
return estimatedSpend;
});
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container && this.container.databaseAccount && this.container.databaseAccount();
const isFreeTierAccount =

View File

@@ -33,6 +33,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
public keyspaceThroughput: ko.Observable<number>;
public keyspaceCreateNew: ko.Observable<boolean>;
public dedicateTableThroughput: ko.Observable<boolean>;
public canRequestSupport: ko.PureComputed<boolean>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public sharedThroughputSpendAck: ko.Observable<boolean>;
@@ -227,6 +228,15 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
return configContext.platform !== Platform.Emulator;
});
this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if (this.isSharedAutoPilotSelected()) {

View File

@@ -3,6 +3,11 @@ export var Int32 = {
Max: 2147483647
};
export var Int64 = {
Min: -9223372036854775808,
Max: 9223372036854775807
};
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";

View File

@@ -30,6 +30,7 @@
class: 'scaleForm dirty',
value: throughput,
minimum: minRUs,
maximum: maxRUThroughputInputLimit,
canExceedMaximumValue: canThroughputExceedMaximumValue,
step: throughputIncreaseFactor,
label: throughputTitle,
@@ -55,6 +56,16 @@
<span>Learn more about minimum throughput </span>
<a href="https://docs.microsoft.com/azure/cosmos-db/set-throughput" target="_blank">here.</a>
</p>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade -->
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request"
>Contact support</a
>
for more than <span data-bind="text: maxRUsText"></span> RU/s
</p>
<p data-bind="visible: shouldDisplayPortalUsePrompt">
Use Data Explorer from Azure Portal to request more than <span data-bind="text: maxRUsText"></span> RU/s
</p>
</div>
</div>
</div>

View File

@@ -17,6 +17,7 @@ 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";
@@ -58,16 +59,23 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
public saveSettingsButton: ViewModels.Button;
public discardSettingsChangesButton: ViewModels.Button;
public canRequestSupport: ko.PureComputed<boolean>;
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
public costsVisible: ko.Computed<boolean>;
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 maxRUsText: ko.PureComputed<string>;
public maxRUThroughputInputLimit: ko.Computed<number>;
public notificationStatusInfo: ko.Observable<string>;
public pendingNotification: ko.Observable<DataModels.Notification>;
public requestUnitsUsageCost: ko.PureComputed<string>;
public autoscaleCost: ko.PureComputed<string>;
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
public shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
public shouldShowStatusBar: ko.Computed<boolean>;
public throughputTitle: ko.PureComputed<string>;
public throughputAriaLabel: ko.PureComputed<string>;
@@ -173,6 +181,22 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return configContext.platform !== Platform.Emulator;
});
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(() => configContext.platform === Platform.Hosted);
this.canThroughputExceedMaximumValue = ko.pureComputed<boolean>(
() => configContext.platform === Platform.Portal && !this.container.isRunningOnNationalCloud()
);
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform === Platform.Emulator ||
configContext.platform === Platform.Hosted ||
this.canThroughputExceedMaximumValue()
) {
return false;
}
return true;
});
this.overrideWithAutoPilotSettings = ko.pureComputed(() => {
return this._hasProvisioningTypeChanged() && this._wasAutopilotOriginallySet();
});
@@ -205,6 +229,34 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
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.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
if (configContext.platform === Platform.Hosted) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
return this.maxRUs();
});
this.maxRUsText = ko.pureComputed(() => {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString();
});
this.throughputTitle = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
@@ -246,10 +298,18 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
}
if (this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million) {
if (
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.canThroughputExceedMaximumValue()
) {
return updateThroughputBeyondLimitWarningMessage;
}
if (this.throughput() > this.maxRUs()) {
return updateThroughputDelayedApplyWarningMessage;
}
if (this.pendingNotification()) {
const matches: string[] = this.pendingNotification().description.match("Throughput update for (.*) RU/s");
const throughput: number = matches.length > 1 && Number(matches[1]);
@@ -308,6 +368,13 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return false;
}
if (
!this.canThroughputExceedMaximumValue() &&
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
) {
return false;
}
if (this.throughput.editableIsDirty()) {
return true;
}
@@ -383,18 +450,40 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
const originalThroughputValue = this.throughput.getEditableOriginalValue();
const newThroughput = this.throughput();
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.database.id(),
currentOffer: this.database.offer(),
autopilotThroughput: undefined,
manualThroughput: newThroughput,
migrateToManual: this._hasProvisioningTypeChanged()
};
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();
const updatedOffer = await updateOffer(updateOfferParams);
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
this.database.offer(updatedOffer);
this.database.offer.valueHasMutated();
}
}
}
} catch (error) {

View File

@@ -57,7 +57,9 @@
class: 'scaleForm dirty',
value: throughput,
minimum: minRUs,
maximum: maxRUThroughputInputLimit,
isEnabled: !hasDatabaseSharedThroughput(),
canExceedMaximumValue: canThroughputExceedMaximumValue,
label: throughputTitle,
ariaLabel: throughputAriaLabel,
costsVisible: costsVisible,

View File

@@ -34,6 +34,17 @@ describe("Settings tab", () => {
collections: [baseCollection]
};
const quotaInfo: DataModels.CollectionQuotaInfo = {
storedProcedures: 0,
triggers: 0,
functions: 0,
documentsSize: 0,
documentsCount: 0,
collectionSize: 0,
usageSizeInKB: 0,
numPartitions: 0
};
describe("Conflict Resolution", () => {
describe("should show conflict resolution", () => {
let explorer: Explorer;
@@ -59,6 +70,7 @@ describe("Settings tab", () => {
explorer,
"mydb",
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
quotaInfo,
null
),
onUpdateTabsButtons: undefined
@@ -174,7 +186,7 @@ describe("Settings tab", () => {
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
@@ -195,7 +207,7 @@ describe("Settings tab", () => {
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
@@ -211,7 +223,7 @@ describe("Settings tab", () => {
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
@@ -246,7 +258,7 @@ describe("Settings tab", () => {
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
@@ -260,7 +272,7 @@ describe("Settings tab", () => {
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
@@ -283,7 +295,7 @@ describe("Settings tab", () => {
tabPath: "",
hashLocation: "",
isActive: ko.observable(false),
collection: new Collection(explorer, "mydb", baseCollection, null),
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
});
@@ -342,6 +354,7 @@ describe("Settings tab", () => {
_ts: 0,
id: "mycoll"
},
quotaInfo,
offer
);
}

View File

@@ -20,6 +20,7 @@ import { updateOffer } from "../../Common/dataAccess/updateOffer";
import { updateCollection } from "../../Common/dataAccess/updateCollection";
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";
@@ -150,6 +151,9 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
public saveSettingsButton: ViewModels.Button;
public discardSettingsChangesButton: ViewModels.Button;
public canRequestSupport: ko.Computed<boolean>;
public canThroughputExceedMaximumValue: ko.Computed<boolean>;
public changeFeedPolicyOffId: string;
public changeFeedPolicyOnId: string;
public changeFeedPolicyToggled: ViewModels.Editable<ChangeFeedPolicyToggledState>;
@@ -170,6 +174,9 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
public indexingPolicyElementFocused: ko.Observable<boolean>;
public minRUs: ko.Computed<number>;
public minRUAnotationVisible: ko.Computed<boolean>;
public maxRUs: ko.Computed<number>;
public maxRUThroughputInputLimit: ko.Computed<number>;
public maxRUsText: ko.PureComputed<string>;
public notificationStatusInfo: ko.Observable<string>;
public partitionKeyName: ko.Computed<string>;
public partitionKeyVisible: ko.PureComputed<boolean>;
@@ -181,6 +188,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
public rupmVisible: ko.Computed<boolean>;
public scaleExpanded: ko.Observable<boolean>;
public settingsExpanded: ko.Observable<boolean>;
public shouldDisplayPortalUsePrompt: ko.Computed<boolean>;
public shouldShowIndexingPolicyEditor: ko.Computed<boolean>;
public shouldShowNotificationStatusPrompt: ko.Computed<boolean>;
public shouldShowStatusBar: ko.Computed<boolean>;
@@ -453,6 +461,43 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
return (this.container && this.container.isTryCosmosDBSubscription()) || false;
});
this.canThroughputExceedMaximumValue = ko.pureComputed<boolean>(() => {
return (
this._isFixedContainer() &&
configContext.platform === Platform.Portal &&
!this.container.isRunningOnNationalCloud()
);
});
this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform === Platform.Emulator) {
return false;
}
if (this.isTryCosmosDBSubscription()) {
return false;
}
if (this.canThroughputExceedMaximumValue()) {
return false;
}
if (configContext.platform === Platform.Hosted) {
return false;
}
if (this.container.isServerlessEnabled()) {
return false;
}
const numPartitions = this.collection.quotaInfo().numPartitions;
return !!this.collection.partitionKeyProperty || numPartitions > 1;
});
this.shouldDisplayPortalUsePrompt = ko.pureComputed<boolean>(
() => configContext.platform === Platform.Hosted && !!this.collection.partitionKey
);
this.minRUs = ko.computed<number>(() => {
if (this.isTryCosmosDBSubscription() || this.container.isServerlessEnabled()) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
@@ -462,7 +507,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
this.collection && this.collection.offer && this.collection.offer() && this.collection.offer().content;
if (offerContent && offerContent.offerAutopilotSettings) {
SharedConstants.CollectionCreation.DefaultCollectionRUs400;
return 400;
}
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
@@ -476,21 +521,72 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
return collectionThroughputInfo.minimumRUForCollection;
}
// minimumRUForCollection should always be present, but just in case return a default
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
const numPartitions =
(collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions) ||
this.collection.quotaInfo().numPartitions;
if (!numPartitions || numPartitions === 1) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
let baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
const quotaInKb = this.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);
});
this.minRUAnotationVisible = ko.computed<boolean>(() => {
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
});
this.maxRUs = ko.computed<number>(() => {
const isTryCosmosDBSubscription = this.isTryCosmosDBSubscription();
if (isTryCosmosDBSubscription || this.container.isServerlessEnabled()) {
return Constants.TryCosmosExperience.maxRU;
}
const numPartitionsFromOffer: number =
this.collection &&
this.collection.offer &&
this.collection.offer() &&
this.collection.offer().content &&
this.collection.offer().content.collectionThroughputInfo &&
this.collection.offer().content.collectionThroughputInfo.numPhysicalPartitions;
const numPartitionsFromQuotaInfo: number = this.collection && this.collection.quotaInfo().numPartitions;
const numPartitions = numPartitionsFromOffer || numPartitionsFromQuotaInfo || 1;
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
});
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
if (configContext.platform === Platform.Hosted && this.collection.partitionKey) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
return this.maxRUs();
});
this.maxRUsText = ko.pureComputed(() => {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million.toLocaleString();
});
this.throughputTitle = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
const minThroughput: string = this.minRUs().toLocaleString();
const maxThroughput: string = !this._isFixedContainer() ? "unlimited" : "10000";
const maxThroughput: string =
this.canThroughputExceedMaximumValue() && !this._isFixedContainer()
? "unlimited"
: this.maxRUs().toLocaleString();
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
});
@@ -579,6 +675,22 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
return false;
}
const isThroughputGreaterThanMaxRus = this.throughput() > this.maxRUs();
const isEmulator = configContext.platform === Platform.Emulator;
if (isThroughputGreaterThanMaxRus && isEmulator) {
return false;
}
if (isThroughputGreaterThanMaxRus && this._isFixedContainer()) {
return false;
}
const isThroughputMoreThan1Million =
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (!this.canThroughputExceedMaximumValue() && isThroughputMoreThan1Million) {
return false;
}
if (this.throughput.editableIsDirty()) {
return true;
}
@@ -602,6 +714,14 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
return false;
}
if (
this.rupm() === Constants.RUPMStates.on &&
this.throughput() >
SharedConstants.CollectionCreation.MaxRUPMPerPartition * this.collection.quotaInfo()?.numPartitions
) {
return false;
}
if (this.timeToLive.editableIsDirty()) {
return true;
}
@@ -721,6 +841,14 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
this.warningMessage = ko.computed<string>(() => {
const throughputExceedsBackendLimits: boolean =
this.canThroughputExceedMaximumValue() &&
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
const throughputExceedsMaxValue: boolean =
configContext.platform !== Platform.Emulator && this.throughput() > this.maxRUs();
const ttlOptionDirty: boolean = this.timeToLive.editableIsDirty();
const ttlOrIndexingPolicyFieldsDirty: boolean =
this.timeToLive.editableIsDirty() ||
@@ -762,6 +890,26 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
return AutoPilotUtils.manualToAutoscaleDisclaimer;
}
if (
throughputExceedsBackendLimits &&
!!this.collection.partitionKey &&
!this._isFixedContainer() &&
!ttlFieldFocused &&
!this.indexingPolicyElementFocused()
) {
return updateThroughputBeyondLimitWarningMessage;
}
if (
throughputExceedsMaxValue &&
!!this.collection.partitionKey &&
!this._isFixedContainer() &&
!ttlFieldFocused &&
!this.indexingPolicyElementFocused()
) {
return updateThroughputDelayedApplyWarningMessage;
}
if (this.pendingNotification()) {
const throughputUnit: string = this._getThroughputUnit();
const matches: string[] = this.pendingNotification().description.match(
@@ -951,23 +1099,54 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}
}
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
manualThroughput: this.isAutoPilotSelected() ? undefined : newThroughput
};
if (this._hasProvisioningTypeChanged()) {
if (this.isAutoPilotSelected()) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
if (
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.container != null
) {
const requestPayload = {
subscriptionId: userContext.subscriptionId,
databaseAccountName: userContext.databaseAccount.name,
resourceGroup: userContext.resourceGroup,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
throughput: newThroughput,
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
};
await updateOfferThroughputBeyondLimit(requestPayload);
this.collection.offer().content.offerThroughput = originalThroughputValue;
this.throughput(originalThroughputValue);
this.notificationStatusInfo(
throughputApplyDelayedMessage(
this.isAutoPilotSelected(),
originalThroughputValue,
this._getThroughputUnit(),
this.collection.databaseId,
this.collection.id(),
newThroughput
)
);
this.throughput.valueHasMutated(); // force component re-render
} else {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
manualThroughput: this.isAutoPilotSelected() ? undefined : newThroughput
};
if (this._hasProvisioningTypeChanged()) {
if (this.isAutoPilotSelected()) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer);
this.collection.offer.valueHasMutated();
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer);
this.collection.offer.valueHasMutated();
}
this.container.isRefreshingExplorer(false);

View File

@@ -10,9 +10,10 @@ describe("Collection", () => {
container: Explorer,
databaseId: string,
data: DataModels.Collection,
quotaInfo: DataModels.CollectionQuotaInfo,
offer: DataModels.Offer
): Collection {
return new Collection(container, databaseId, data, offer);
return new Collection(container, databaseId, data, quotaInfo, offer);
}
function generateMockCollectionsDataModelWithPartitionKey(
@@ -49,7 +50,7 @@ describe("Collection", () => {
});
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
return generateCollection(mockContainer, "abc", data, {} as DataModels.CollectionQuotaInfo, {} as DataModels.Offer);
}
describe("Partition key path parsing", () => {

View File

@@ -10,6 +10,7 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -40,8 +41,11 @@ import { userContext } from "../../UserContext";
import TabsBase from "../Tabs/TabsBase";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import promiseRetry from "p-retry";
export default class Collection implements ViewModels.Collection {
private static readonly LoadOfferMaxRetries = 5;
private static readonly LoadOfferRetryAfterMilliSeconds = 5000;
public nodeKind: string;
public container: Explorer;
public self: string;
@@ -54,6 +58,7 @@ export default class Collection implements ViewModels.Collection {
public defaultTtl: ko.Observable<number>;
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
public offer: ko.Observable<DataModels.Offer>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
@@ -92,7 +97,13 @@ export default class Collection implements ViewModels.Collection {
public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>;
constructor(container: Explorer, databaseId: string, data: DataModels.Collection, offer: DataModels.Offer) {
constructor(
container: Explorer,
databaseId: string,
data: DataModels.Collection,
quotaInfo: DataModels.CollectionQuotaInfo,
offer: DataModels.Offer
) {
this.nodeKind = "Collection";
this.container = container;
this.self = data._self;
@@ -104,6 +115,7 @@ export default class Collection implements ViewModels.Collection {
this.id = ko.observable(data.id);
this.defaultTtl = ko.observable(data.defaultTtl);
this.indexingPolicy = ko.observable(data.indexingPolicy);
this.quotaInfo = ko.observable(quotaInfo);
this.offer = ko.observable(offer);
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
@@ -660,6 +672,14 @@ export default class Collection implements ViewModels.Collection {
}
};
private async loadCollectionQuotaInfo(): Promise<void> {
// TODO: Use the collection entity cache to get quota info
const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);
this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy;
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
this.quotaInfo(quotaInfo);
}
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
@@ -1314,6 +1334,20 @@ export default class Collection implements ViewModels.Collection {
return this.container.findDatabaseWithId(this.databaseId);
}
public async loadAutopilotOfferWithRetry(): Promise<DataModels.Offer> {
let retryCount = 0;
while (retryCount < Collection.LoadOfferMaxRetries) {
if (this.offer().content.offerAutopilotSettings) {
return this.offer();
} else {
await new Promise(resolve => setTimeout(resolve, Collection.LoadOfferRetryAfterMilliSeconds));
await this.loadOffer();
}
retryCount++;
}
throw new Error("Max retries for loading offer are completed. Offer does not have an Autopilot settings field.");
}
public async loadOffer(): Promise<void> {
if (!this.container.isServerlessEnabled() && !this.offer()) {
this.container.isRefreshingExplorer(true);
@@ -1332,6 +1366,7 @@ export default class Collection implements ViewModels.Collection {
try {
this.offer(await readCollectionOffer(params));
await this.loadCollectionQuotaInfo();
TelemetryProcessor.traceSuccess(
Action.LoadOffers,

View File

@@ -185,7 +185,7 @@ export default class Database implements ViewModels.Database {
const deltaCollections = this.getDeltaCollections(collections);
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null);
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
collectionVMs.push(collectionVM);
});