import * as ViewModels from "../../../Contracts/ViewModels"; import * as DataModels from "../../../Contracts/DataModels"; import * as Constants from "../../../Common/Constants"; import * as SharedConstants from "../../../Shared/Constants"; import * as PricingUtils from "../../../Utils/PricingUtils"; import Explorer from "../../Explorer"; import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types"; const zeroValue = 0; export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy; export const TtlOff = "off"; export const TtlOn = "on"; export const TtlOnNoDefault = "on-nodefault"; export const MongoIndexIdField = "_id"; export const MongoWildcardPlaceHolder = "properties.$**"; export const SingleFieldText = "Single Field"; export const WildcardText = "Wildcard"; export enum ChangeFeedPolicyState { Off = "Off", On = "On" } export enum TtlType { Off = "off", On = "on", OnNoDefault = "on-nodefault" } export enum GeospatialConfigType { Geography = "Geography", Geometry = "Geometry" } export enum MongoIndexTypes { Single = "Single", Wildcard = "Wildcard" } export interface AddMongoIndexProps { mongoIndex: MongoIndex; type: MongoIndexTypes; notification: MongoNotificationMessage; } export enum SettingsV2TabTypes { ScaleTab, ConflictResolutionTab, SubSettingsTab, IndexingPolicyTab } export interface IsComponentDirtyResult { isSaveable: boolean; isDiscardable: boolean; } export enum MongoNotificationType { Warning = "Warning", Error = "Error" } export interface MongoNotificationMessage { type: MongoNotificationType; message: string; } export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): boolean => { const database: ViewModels.Database = collection.getDatabase(); return database?.isDatabaseShared() && !collection.offer(); }; export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => { const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false; if (isTryCosmosDBSubscription) { return Constants.TryCosmosExperience.maxRU; } const numPartitionsFromOffer: number = collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions; const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions; const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1; return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions; }; export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => { const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription(); if (isTryCosmosDBSubscription) { return SharedConstants.CollectionCreation.DefaultCollectionRUs400; } const offerContent = collection?.offer && collection.offer()?.content; if (offerContent?.offerAutopilotSettings) { return SharedConstants.CollectionCreation.DefaultCollectionRUs400; } const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo; if (collectionThroughputInfo?.minimumRUForCollection > 0) { return collectionThroughputInfo.minimumRUForCollection; } const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions; if (!numPartitions || numPartitions === 1) { return SharedConstants.CollectionCreation.DefaultCollectionRUs400; } const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400; const quotaInKb = collection.quotaInfo().collectionSize; const quotaInGb = PricingUtils.usageInGB(quotaInKb); const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions); const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100; return Math.max(baseRU, baseRUbyPartitions); }; export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => { // Backend can contain different casing as it does case-insensitive comparisson if (!modeFromBackend) { return undefined; } const modeAsLowerCase: string = modeFromBackend.toLowerCase(); if (modeAsLowerCase === DataModels.ConflictResolutionMode.Custom.toLowerCase()) { return DataModels.ConflictResolutionMode.Custom; } // Default is LWW return DataModels.ConflictResolutionMode.LastWriterWins; }; export const parseConflictResolutionProcedure = (procedureFromBackEnd: string): string => { // Backend data comes in /dbs/xxxx/colls/xxxx/sprocs/{name}, to make it easier for users, we just use the name if (!procedureFromBackEnd) { return undefined; } if (procedureFromBackEnd.indexOf("/") >= 0) { const sprocsIndex: number = procedureFromBackEnd.indexOf(Constants.HashRoutePrefixes.sprocHash); if (sprocsIndex === -1) { return undefined; } return procedureFromBackEnd.substr(sprocsIndex + Constants.HashRoutePrefixes.sprocHash.length); } // No path, just a name, in case backend returns just the name return procedureFromBackEnd; }; export const getSanitizedInputValue = (newValueString: string, max: number): number => { const newValue = parseInt(newValueString); if (isNaN(newValue)) { return zeroValue; } // make sure new value does not exceed the maximum throughput return Math.min(newValue, max); }; export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => { const currentType = typeof current; const baselineType = typeof baseline; if (currentType !== "undefined" && baselineType !== "undefined" && currentType !== baselineType) { throw new Error("current and baseline values are not of the same type."); } const currentStringValue = getStringValue(current, currentType); const baselineStringValue = getStringValue(baseline, baselineType); return currentStringValue !== baselineStringValue; }; const getStringValue = (value: isDirtyTypes, type: string): string => { switch (type) { case "string": case "undefined": case "number": case "boolean": return value?.toString(); default: return JSON.stringify(value); } }; export const getTabTitle = (tab: SettingsV2TabTypes): string => { switch (tab) { case SettingsV2TabTypes.ScaleTab: return "Scale"; case SettingsV2TabTypes.ConflictResolutionTab: return "Conflict Resolution"; case SettingsV2TabTypes.SubSettingsTab: return "Settings"; case SettingsV2TabTypes.IndexingPolicyTab: return "Indexing Policy"; default: throw new Error(`Unknown tab ${tab}`); } }; export const getMongoNotification = (description: string, type: MongoIndexTypes): MongoNotificationMessage => { if (description && !type) { return { type: MongoNotificationType.Warning, message: "Please select a type for each index." }; } if (type && (!description || description.trim().length === 0)) { return { type: MongoNotificationType.Error, message: "Please enter a field name." }; } else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) { return { type: MongoNotificationType.Error, message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder }; } return undefined; }; export const getMongoIndexType = (keys: string[]): MongoIndexTypes => { const length = keys?.length; let type: MongoIndexTypes; if (length === 1) { if (keys[0].indexOf("$**") !== -1) { type = MongoIndexTypes.Wildcard; } else { type = MongoIndexTypes.Single; } } return type; }; export const getMongoIndexTypeText = (index: MongoIndexTypes): string => { if (index === MongoIndexTypes.Single) { return SingleFieldText; } return WildcardText; }; export const isIndexTransforming = (indexTransformationProgress: number): boolean => // index transformation progress can be 0 indexTransformationProgress !== undefined && indexTransformationProgress !== 100;