mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-28 21:32:05 +00:00
* Index refresh component introduced - Made all notifications in Mongo Index editor have 12 font size - Added indexing policy refresh to sql indexing policy editor - Added "you have unsaved changes" message, replace old message for lazy indexing policy changes * formatting changes * addressed PR comments
257 lines
7.9 KiB
TypeScript
257 lines
7.9 KiB
TypeScript
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;
|