cosmos-explorer/src/Explorer/Controls/Settings/SettingsComponent.tsx
Srinath Narayanan b4219e2994
Added Support for editing Mongo Indexing Policy from Settings tab (#284)
* added  SettingsV2 Tab

* lint changes

* foxed failing test

* Addressed PR comments

- removed dangerouslySetInnerHtml
- removed underscore dependency
- added AccessibleElement
- removed unnecesary exceptions to linting

* split render into separate functions

- removed sinon in test
- Added some enums to replace constant strings
- removed dangerously set inner html
- made autopilot input as StatefulValue

* add settingscomponent snapshot

* fixed linting errors

* fixed errors

* addressed PR comments

- Moved StatefulValue to new class
- Split render to more functions for throughputInputComponents

* Added sub components

- Added tests for SettingsRenderUtls
- Added empty test files for adding tests later

* Moved all inputs to fluent UI

- removed rupm
- added reusable styles

* Added Tabs

- Added ToolTipLabel component
- Removed toggleables for individual components
- Removed accessible elements
- Added IndexingPolicyComponent

* Added more tests

* Addressed PR comments

* Moved Label radio buttons to choicegroup

* fixed lint errors

* Removed StatefulValue

- Moved conflict res tab to the end
- Added styling for autpilot radiobuttons

* fixed linting errors

* fix bugs from merge to master

* fixed formatting issue

* Addressed PR comments

- Added unit tests for smaller methods within each component

* fixed linting errors

* removed redundant snapshots

* removed empty line

* made separate props objects for subcomponents

* Moved dirty checks to sub components

* Made indesing policy component height = 80% of view port

- modified auto pilot v3 messages
- Added Fluent UI tolltip
-

* Moved warning messages inline

* moved conflict res helpers out

* fixed bugs

* added stack style for message

* fixed tests

* Added tests

* fixed linting and format errors

* undid changes

* more edits

* fixed compile errors

* fixed compile errors

* fixed errors

* fixed bug with save and discard buttons

* fixed compile errors

* added MongoIndexingPolicy component

* addressed PR comments

* moved read indexes to scale context

* added add index feature

* added AddMongoIndexComponent

* Added collapsible portions and focus changes

* removed unnecessary imports

* finetuned UI

* more edits

* Added mongoindexeditor flight

- Moved add index UI to within current index pane

* minro edits

* Added separate warning messages for index refresh

* aligned items

* Fixed tests

* minor edits

* resolved PR comments

* modified refs usage

* compile errors fixed

* moved fetch of notifications and offer to within the tab activation

* fixed PR comments

* added error handling

* added AAD verification

* removed l empty line

* added back line

* deleted file

* added file

* addressed PR comments

* addressed PR comments

* fixed format error

* updated package.json

* updated package-lock.json
2020-10-26 14:17:40 -07:00

1019 lines
40 KiB
TypeScript

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 { throughputUnit } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import {
MongoIndexingPolicyComponent,
MongoIndexingPolicyComponentProps
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
import {
getMaxRUs,
hasDatabaseSharedThroughput,
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
SettingsV2TabTypes,
getTabTitle,
isDirty,
AddMongoIndexProps,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
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 {
getMongoDBCollectionIndexTransformationProgress,
readMongoDBCollectionThroughRP
} from "../../../Common/dataAccess/readMongoDBCollection";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
content: JSX.Element;
}
interface ButtonV2 {
isVisible: () => boolean;
isEnabled: () => boolean;
isSelected?: () => boolean;
}
export interface SettingsComponentProps {
settingsTab: SettingsTab;
}
export interface SettingsComponentState {
throughput: number;
throughputBaseline: number;
autoPilotThroughput: number;
autoPilotThroughputBaseline: number;
isAutoPilotSelected: boolean;
wasAutopilotOriginallySet: boolean;
isScaleSaveable: boolean;
isScaleDiscardable: boolean;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
timeToLiveSeconds: number;
timeToLiveSecondsBaseline: number;
geospatialConfigType: GeospatialConfigType;
geospatialConfigTypeBaseline: GeospatialConfigType;
analyticalStorageTtlSelection: TtlType;
analyticalStorageTtlSelectionBaseline: TtlType;
analyticalStorageTtlSeconds: number;
analyticalStorageTtlSecondsBaseline: number;
changeFeedPolicy: ChangeFeedPolicyState;
changeFeedPolicyBaseline: ChangeFeedPolicyState;
isSubSettingsSaveable: boolean;
isSubSettingsDiscardable: boolean;
indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
shouldDiscardIndexingPolicy: boolean;
isIndexingPolicyDirty: boolean;
isMongoIndexingPolicySaveable: boolean;
isMongoIndexingPolicyDiscardable: boolean;
currentMongoIndexes: MongoIndex[];
indexesToDrop: number[];
indexesToAdd: AddMongoIndexProps[];
indexTransformationProgress: number;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
conflictResolutionPolicyPath: string;
conflictResolutionPolicyPathBaseline: string;
conflictResolutionPolicyProcedure: string;
conflictResolutionPolicyProcedureBaseline: string;
isConflictResolutionDirty: boolean;
initialNotification: DataModels.Notification;
selectedTab: SettingsV2TabTypes;
}
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
private static readonly sixMonthsInSeconds = 15768000;
private saveSettingsButton: ButtonV2;
private discardSettingsChangesButton: ButtonV2;
private isAnalyticalStorageEnabled: boolean;
private collection: ViewModels.Collection;
private container: Explorer;
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
private autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
private shouldShowIndexingPolicyEditor: boolean;
public mongoDBCollectionResource: MongoDBCollectionResource;
constructor(props: SettingsComponentProps) {
super(props);
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.container = this.collection?.container;
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy
);
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
!this.collection.partitionKey ||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
this.state = {
throughput: undefined,
throughputBaseline: undefined,
autoPilotThroughput: undefined,
autoPilotThroughputBaseline: undefined,
isAutoPilotSelected: false,
wasAutopilotOriginallySet: false,
isScaleSaveable: false,
isScaleDiscardable: false,
timeToLive: undefined,
timeToLiveBaseline: undefined,
timeToLiveSeconds: undefined,
timeToLiveSecondsBaseline: undefined,
geospatialConfigType: undefined,
geospatialConfigTypeBaseline: undefined,
analyticalStorageTtlSelection: undefined,
analyticalStorageTtlSelectionBaseline: undefined,
analyticalStorageTtlSeconds: undefined,
analyticalStorageTtlSecondsBaseline: undefined,
changeFeedPolicy: undefined,
changeFeedPolicyBaseline: undefined,
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
indexingPolicyContent: undefined,
indexingPolicyContentBaseline: undefined,
shouldDiscardIndexingPolicy: false,
isIndexingPolicyDirty: false,
indexesToDrop: [],
indexesToAdd: [],
currentMongoIndexes: undefined,
isMongoIndexingPolicySaveable: false,
isMongoIndexingPolicyDiscardable: false,
indexTransformationProgress: undefined,
conflictResolutionPolicyMode: undefined,
conflictResolutionPolicyModeBaseline: undefined,
conflictResolutionPolicyPath: undefined,
conflictResolutionPolicyPathBaseline: undefined,
conflictResolutionPolicyProcedure: undefined,
conflictResolutionPolicyProcedureBaseline: undefined,
isConflictResolutionDirty: false,
initialNotification: undefined,
selectedTab: SettingsV2TabTypes.ScaleTab
};
this.saveSettingsButton = {
isEnabled: this.isSaveSettingsButtonEnabled,
isVisible: () => {
return true;
}
};
this.discardSettingsChangesButton = {
isEnabled: this.isDiscardSettingsButtonEnabled,
isVisible: () => {
return true;
}
};
}
componentDidMount(): void {
this.loadMongoIndexes();
this.setAutoPilotStates();
this.setBaseline();
if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
}
}
componentDidUpdate(): void {
if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
}
}
public loadMongoIndexes = async (): Promise<void> => {
if (
this.container.isMongoIndexEditorEnabled() &&
this.container.isPreferredApiMongoDB() &&
this.container.databaseAccount()
) {
await this.refreshIndexTransformationProgress();
this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP(
this.collection.databaseId,
this.collection.id()
);
if (this.mongoDBCollectionResource) {
this.setState({
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
});
}
}
};
public refreshIndexTransformationProgress = async (): Promise<void> => {
const currentProgress = await getMongoDBCollectionIndexTransformationProgress(
this.collection.databaseId,
this.collection.id()
);
this.setState({ indexTransformationProgress: currentProgress });
};
public isSaveSettingsButtonEnabled = (): boolean => {
if (this.isOfferReplacePending()) {
return false;
}
return (
this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicySaveable)
);
};
public isDiscardSettingsButtonEnabled = (): boolean => {
return (
this.state.isScaleDiscardable ||
this.state.isSubSettingsDiscardable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty ||
(!!this.state.currentMongoIndexes && this.state.isMongoIndexingPolicyDiscardable)
);
};
private setAutoPilotStates = (): void => {
const offer = this.collection?.offer && this.collection.offer();
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
if (
offerAutopilotSettings &&
offerAutopilotSettings.maxThroughput &&
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
) {
this.setState({
isAutoPilotSelected: true,
wasAutopilotOriginallySet: true,
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
});
}
};
public hasProvisioningTypeChanged = (): boolean =>
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection);
public hasConflictResolution = (): boolean =>
this.container?.databaseAccount &&
this.container.databaseAccount()?.properties?.enableMultipleWriteLocations &&
this.collection.conflictResolutionPolicy &&
!!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]
);
};
public onSaveClick = async (): Promise<void> => {
this.props.settingsTab.isExecutionError(false);
this.props.settingsTab.isExecuting(true);
const startKey: number = traceStart(Action.SettingsV2Updated, {
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
});
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
try {
if (
this.state.isSubSettingsSaveable ||
this.state.isIndexingPolicyDirty ||
this.state.isConflictResolutionDirty
) {
let defaultTtl: number;
switch (this.state.timeToLive) {
case TtlType.On:
defaultTtl = Number(this.state.timeToLiveSeconds);
break;
case TtlType.OnNoDefault:
defaultTtl = -1;
break;
case TtlType.Off:
default:
defaultTtl = undefined;
break;
}
newCollection.defaultTtl = defaultTtl;
newCollection.indexingPolicy = this.state.indexingPolicyContent;
newCollection.changeFeedPolicy =
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
? {
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
}
: undefined;
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
newCollection.geospatialConfig = {
type: this.state.geospatialConfigType
};
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
if (conflictResolutionChanges) {
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
}
const updatedCollection: DataModels.Collection = await updateCollection(
this.collection.databaseId,
this.collection.id(),
newCollection
);
this.collection.rawDataModel = updatedCollection;
this.collection.defaultTtl(updatedCollection.defaultTtl);
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
this.collection.id(updatedCollection.id);
this.collection.indexingPolicy(updatedCollection.indexingPolicy);
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
this.setState({
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false,
isConflictResolutionDirty: false
});
}
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
const newMongoIndexes = this.getMongoIndexesToSave();
const newMongoCollection: MongoDBCollectionResource = {
...this.mongoDBCollectionResource,
indexes: newMongoIndexes
};
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.collection.databaseId,
this.collection.id(),
newMongoCollection
);
await this.refreshIndexTransformationProgress();
this.setState({
isMongoIndexingPolicySaveable: false,
indexesToDrop: [],
indexesToAdd: [],
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
});
}
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 });
if (this.state.isAutoPilotSelected) {
this.setState({
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
});
} else {
this.setState({
throughput: updatedOffer.content.offerThroughput,
throughputBaseline: updatedOffer.content.offerThroughput
});
}
}
}
this.container.isRefreshingExplorer(false);
this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
},
startKey
);
} catch (reason) {
this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true);
console.error(reason);
traceFailure(
Action.SettingsV2Updated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: reason
},
startKey
);
}
this.props.settingsTab.isExecuting(false);
};
public onRevertClick = (): void => {
trace(Action.SettingsV2Discarded, ActionModifiers.Mark, {
message: "Settings Discarded"
});
this.setState({
throughput: this.state.throughputBaseline,
timeToLive: this.state.timeToLiveBaseline,
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
indexesToAdd: [],
indexesToDrop: [],
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyModeBaseline,
conflictResolutionPolicyPath: this.state.conflictResolutionPolicyPathBaseline,
conflictResolutionPolicyProcedure: this.state.conflictResolutionPolicyProcedureBaseline,
analyticalStorageTtlSelection: this.state.analyticalStorageTtlSelectionBaseline,
analyticalStorageTtlSeconds: this.state.analyticalStorageTtlSecondsBaseline,
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
shouldDiscardIndexingPolicy: true,
isScaleSaveable: false,
isScaleDiscardable: false,
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
isIndexingPolicyDirty: false,
isMongoIndexingPolicySaveable: false,
isMongoIndexingPolicyDiscardable: false,
isConflictResolutionDirty: false
});
};
private getMongoIndexesToSave = (): MongoIndex[] => {
let finalIndexes: MongoIndex[] = [];
this.state.currentMongoIndexes?.map((mongoIndex: MongoIndex, index: number) => {
if (!this.state.indexesToDrop.includes(index)) {
finalIndexes.push(mongoIndex);
}
});
finalIndexes = finalIndexes.concat(this.state.indexesToAdd.map((m: AddMongoIndexProps) => m.mongoIndex));
return finalIndexes;
};
private onScaleSaveableChange = (isScaleSaveable: boolean): void =>
this.setState({ isScaleSaveable: isScaleSaveable });
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
this.setState({ isScaleDiscardable: isScaleDiscardable });
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
this.setState({ indexingPolicyContent: newIndexingPolicy });
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
private logIndexingPolicySuccessMessage = (): void => {
if (this.props.settingsTab.onLoadStartKey) {
traceSuccess(
Action.Tab,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
},
this.props.settingsTab.onLoadStartKey
);
this.props.settingsTab.onLoadStartKey = undefined;
}
};
private onIndexDrop = (index: number): void => this.setState({ indexesToDrop: [...this.state.indexesToDrop, index] });
private onRevertIndexDrop = (index: number): void => {
const indexesToDrop = [...this.state.indexesToDrop];
indexesToDrop.splice(index, 1);
this.setState({ indexesToDrop });
};
private onRevertIndexAdd = (index: number): void => {
const indexesToAdd = [...this.state.indexesToAdd];
indexesToAdd.splice(index, 1);
this.setState({ indexesToAdd });
};
private onIndexAddOrChange = (index: number, description: string, type: MongoIndexTypes): void => {
const newIndexesToAdd = [...this.state.indexesToAdd];
const notification = getMongoNotification(description, type);
const newMongoIndexWithType: AddMongoIndexProps = {
mongoIndex: { key: { keys: [description] } } as MongoIndex,
type: type,
notification: notification
};
if (index === newIndexesToAdd.length) {
newIndexesToAdd.push(newMongoIndexWithType);
} else {
newIndexesToAdd[index] = newMongoIndexWithType;
}
this.setState({ indexesToAdd: newIndexesToAdd });
};
private onConflictResolutionPolicyModeChange = (newMode: DataModels.ConflictResolutionMode): void =>
this.setState({ conflictResolutionPolicyMode: newMode });
private onConflictResolutionPolicyPathChange = (newPath: string): void =>
this.setState({ conflictResolutionPolicyPath: newPath });
private onConflictResolutionPolicyProcedureChange = (newProcedure: string): void =>
this.setState({ conflictResolutionPolicyProcedure: newProcedure });
private onConflictResolutionDirtyChange = (isConflictResolutionDirty: boolean): void =>
this.setState({ isConflictResolutionDirty: isConflictResolutionDirty });
private onTtlChange = (newTtl: TtlType): void => this.setState({ timeToLive: newTtl });
private onTimeToLiveSecondsChange = (newTimeToLiveSeconds: number): void =>
this.setState({ timeToLiveSeconds: newTimeToLiveSeconds });
private onGeoSpatialConfigTypeChange = (newGeoSpatialConfigType: GeospatialConfigType): void =>
this.setState({ geospatialConfigType: newGeoSpatialConfigType });
private onAnalyticalStorageTtlSelectionChange = (newAnalyticalStorageTtlSelection: TtlType): void =>
this.setState({ analyticalStorageTtlSelection: newAnalyticalStorageTtlSelection });
private onAnalyticalStorageTtlSecondsChange = (newAnalyticalStorageTtlSeconds: number): void =>
this.setState({ analyticalStorageTtlSeconds: newAnalyticalStorageTtlSeconds });
private onChangeFeedPolicyChange = (newChangeFeedPolicy: ChangeFeedPolicyState): void =>
this.setState({ changeFeedPolicy: newChangeFeedPolicy });
private onSubSettingsSaveableChange = (isSubSettingsSaveable: boolean): void =>
this.setState({ isSubSettingsSaveable: isSubSettingsSaveable });
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
private onMongoIndexingPolicySaveableChange = (isMongoIndexingPolicySaveable: boolean): void =>
this.setState({ isMongoIndexingPolicySaveable });
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
this.setState({ isMongoIndexingPolicyDiscardable });
public getAnalyticalStorageTtl = (): number => {
if (this.isAnalyticalStorageEnabled) {
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
return Number(this.state.analyticalStorageTtlSeconds);
} else {
return Constants.AnalyticalStorageTtl.Infinite;
}
}
return undefined;
};
public getUpdatedConflictResolutionPolicy = (): DataModels.ConflictResolutionPolicy => {
if (
!isDirty(this.state.conflictResolutionPolicyMode, this.state.conflictResolutionPolicyModeBaseline) &&
!isDirty(this.state.conflictResolutionPolicyPath, this.state.conflictResolutionPolicyPathBaseline) &&
!isDirty(this.state.conflictResolutionPolicyProcedure, this.state.conflictResolutionPolicyProcedureBaseline)
) {
return undefined;
}
const policy: DataModels.ConflictResolutionPolicy = {
mode: parseConflictResolutionMode(this.state.conflictResolutionPolicyMode)
};
if (
policy.mode === DataModels.ConflictResolutionMode.Custom &&
this.state.conflictResolutionPolicyProcedure?.length > 0
) {
policy.conflictResolutionProcedure = Constants.HashRoutePrefixes.sprocWithIds(
this.collection.databaseId,
this.collection.id(),
this.state.conflictResolutionPolicyProcedure,
false
);
}
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
if (policy.conflictResolutionPath?.startsWith("/")) {
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
}
}
return policy;
};
public setBaseline = (): void => {
const defaultTtl = this.collection.defaultTtl();
let timeToLive: TtlType = this.state.timeToLive;
let timeToLiveSeconds = this.state.timeToLiveSeconds;
switch (defaultTtl) {
case undefined:
case 0:
timeToLive = TtlType.Off;
timeToLiveSeconds = SettingsComponent.sixMonthsInSeconds;
break;
case -1:
timeToLive = TtlType.OnNoDefault;
timeToLiveSeconds = SettingsComponent.sixMonthsInSeconds;
break;
default:
timeToLive = TtlType.On;
timeToLiveSeconds = defaultTtl;
break;
}
let analyticalStorageTtlSelection: TtlType;
let analyticalStorageTtlSeconds: number;
if (this.isAnalyticalStorageEnabled) {
const analyticalStorageTtl: number = this.collection.analyticalStorageTtl();
if (analyticalStorageTtl === Constants.AnalyticalStorageTtl.Infinite) {
analyticalStorageTtlSelection = TtlType.OnNoDefault;
} else {
analyticalStorageTtlSelection = TtlType.On;
analyticalStorageTtlSeconds = analyticalStorageTtl;
}
}
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off;
const indexingPolicyContent = this.collection.indexingPolicy();
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
conflictResolutionPolicy?.conflictResolutionProcedure
);
const geospatialConfigTypeString: string =
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
this.setState({
throughput: offerThroughput,
throughputBaseline: offerThroughput,
changeFeedPolicy: changeFeedPolicy,
changeFeedPolicyBaseline: changeFeedPolicy,
timeToLive: timeToLive,
timeToLiveBaseline: timeToLive,
timeToLiveSeconds: timeToLiveSeconds,
timeToLiveSecondsBaseline: timeToLiveSeconds,
analyticalStorageTtlSelection: analyticalStorageTtlSelection,
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
indexingPolicyContent: indexingPolicyContent,
indexingPolicyContentBaseline: indexingPolicyContent,
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
conflictResolutionPolicyModeBaseline: conflictResolutionPolicyMode,
conflictResolutionPolicyPath: conflictResolutionPolicyPath,
conflictResolutionPolicyPathBaseline: conflictResolutionPolicyPath,
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure,
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
geospatialConfigType: geoSpatialConfigType,
geospatialConfigTypeBaseline: geoSpatialConfigType
});
};
private getTabsButtons = (): CommandButtonComponentProps[] => {
const buttons: CommandButtonComponentProps[] = [];
if (this.saveSettingsButton.isVisible()) {
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
onCommandClick: async () => await this.onSaveClick(),
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveSettingsButton.isEnabled()
});
}
if (this.discardSettingsChangesButton.isVisible()) {
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
onCommandClick: this.onRevertClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.discardSettingsChangesButton.isEnabled()
});
}
return buttons;
};
private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
this.setState({ autoPilotThroughput: newThroughput });
private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput });
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
private onPivotChange = (item: PivotItem): void => {
const selectedTab = SettingsV2TabTypes[item.props.itemKey as keyof typeof SettingsV2TabTypes];
this.setState({ selectedTab: selectedTab });
};
public render(): JSX.Element {
const scaleComponentProps: ScaleComponentProps = {
collection: this.collection,
container: this.container,
isFixedContainer: this.isFixedContainer,
autoPilotTiersList: this.autoPilotTiersList,
onThroughputChange: this.onThroughputChange,
throughput: this.state.throughput,
throughputBaseline: this.state.throughputBaseline,
autoPilotThroughput: this.state.autoPilotThroughput,
autoPilotThroughputBaseline: this.state.autoPilotThroughputBaseline,
isAutoPilotSelected: this.state.isAutoPilotSelected,
wasAutopilotOriginallySet: this.state.wasAutopilotOriginallySet,
onAutoPilotSelected: this.onAutoPilotSelected,
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
onScaleSaveableChange: this.onScaleSaveableChange,
onScaleDiscardableChange: this.onScaleDiscardableChange,
initialNotification: this.props.settingsTab.pendingNotification()
};
const subSettingsComponentProps: SubSettingsComponentProps = {
collection: this.collection,
container: this.container,
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,
changeFeedPolicyVisible: this.changeFeedPolicyVisible,
timeToLive: this.state.timeToLive,
timeToLiveBaseline: this.state.timeToLiveBaseline,
onTtlChange: this.onTtlChange,
timeToLiveSeconds: this.state.timeToLiveSeconds,
timeToLiveSecondsBaseline: this.state.timeToLiveSecondsBaseline,
onTimeToLiveSecondsChange: this.onTimeToLiveSecondsChange,
geospatialConfigType: this.state.geospatialConfigType,
geospatialConfigTypeBaseline: this.state.geospatialConfigTypeBaseline,
onGeoSpatialConfigTypeChange: this.onGeoSpatialConfigTypeChange,
analyticalStorageTtlSelection: this.state.analyticalStorageTtlSelection,
analyticalStorageTtlSelectionBaseline: this.state.analyticalStorageTtlSelectionBaseline,
onAnalyticalStorageTtlSelectionChange: this.onAnalyticalStorageTtlSelectionChange,
analyticalStorageTtlSeconds: this.state.analyticalStorageTtlSeconds,
analyticalStorageTtlSecondsBaseline: this.state.analyticalStorageTtlSecondsBaseline,
onAnalyticalStorageTtlSecondsChange: this.onAnalyticalStorageTtlSecondsChange,
changeFeedPolicy: this.state.changeFeedPolicy,
changeFeedPolicyBaseline: this.state.changeFeedPolicyBaseline,
onChangeFeedPolicyChange: this.onChangeFeedPolicyChange,
onSubSettingsSaveableChange: this.onSubSettingsSaveableChange,
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange
};
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
indexingPolicyContent: this.state.indexingPolicyContent,
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
};
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
mongoIndexes: this.state.currentMongoIndexes,
onIndexDrop: this.onIndexDrop,
indexesToDrop: this.state.indexesToDrop,
onRevertIndexDrop: this.onRevertIndexDrop,
indexesToAdd: this.state.indexesToAdd,
onRevertIndexAdd: this.onRevertIndexAdd,
onIndexAddOrChange: this.onIndexAddOrChange,
indexTransformationProgress: this.state.indexTransformationProgress,
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
onMongoIndexingPolicySaveableChange: this.onMongoIndexingPolicySaveableChange,
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange
};
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
collection: this.collection,
container: this.container,
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
conflictResolutionPolicyModeBaseline: this.state.conflictResolutionPolicyModeBaseline,
onConflictResolutionPolicyModeChange: this.onConflictResolutionPolicyModeChange,
conflictResolutionPolicyPath: this.state.conflictResolutionPolicyPath,
conflictResolutionPolicyPathBaseline: this.state.conflictResolutionPolicyPathBaseline,
onConflictResolutionPolicyPathChange: this.onConflictResolutionPolicyPathChange,
conflictResolutionPolicyProcedure: this.state.conflictResolutionPolicyProcedure,
conflictResolutionPolicyProcedureBaseline: this.state.conflictResolutionPolicyProcedureBaseline,
onConflictResolutionPolicyProcedureChange: this.onConflictResolutionPolicyProcedureChange,
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange
};
const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.collection.offer()) {
tabs.push({
tab: SettingsV2TabTypes.ScaleTab,
content: <ScaleComponent {...scaleComponentProps} />
});
}
tabs.push({
tab: SettingsV2TabTypes.SubSettingsTab,
content: <SubSettingsComponent {...subSettingsComponentProps} />
});
if (this.shouldShowIndexingPolicyEditor) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
});
} else if (
this.container.isMongoIndexEditorEnabled() &&
this.container.isPreferredApiMongoDB() &&
this.container.isEnableMongoCapabilityPresent()
) {
tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
});
}
if (this.hasConflictResolution()) {
tabs.push({
tab: SettingsV2TabTypes.ConflictResolutionTab,
content: <ConflictResolutionComponent {...conflictResolutionPolicyComponentProps} />
});
}
const pivotProps: IPivotProps = {
onLinkClick: this.onPivotChange,
selectedKey: SettingsV2TabTypes[this.state.selectedTab]
};
const pivotItems = tabs.map(tab => {
const pivotItemProps: IPivotItemProps = {
itemKey: SettingsV2TabTypes[tab.tab],
style: { marginTop: 20 },
headerText: getTabTitle(tab.tab)
};
return (
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
{tab.content}
</PivotItem>
);
});
return (
<div className="settingsV2MainContainer">
{this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>This table shared throughput is configured at the keyspace</div>
)}
<div className="settingsV2TabsContainer">
<Pivot {...pivotProps}>{pivotItems}</Pivot>
</div>
</div>
);
}
}