import { DirectionalHint, Dropdown, DropdownMenuItemType, Icon, IDropdownOption, Link, Separator, Stack, Text, TooltipHost, } from "@fluentui/react"; import * as Constants from "Common/Constants"; import { createGlobalSecondaryIndex } from "Common/dataAccess/createMaterializedView"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import * as DataModels from "Contracts/DataModels"; import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels"; import { Collection, Database } from "Contracts/ViewModels"; import Explorer from "Explorer/Explorer"; import { AllPropertiesIndexed, FullTextPolicyDefault, getPartitionKey, isSynapseLinkEnabled, scrollToSection, shouldShowAnalyticalStoreOptions, } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; import { chooseSourceContainerStyle, chooseSourceContainerStyles, } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles"; import { AdvancedComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent"; import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent"; import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent"; import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent"; import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent"; import { VectorSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent"; import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent"; import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"; import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen"; import { useDatabases } from "Explorer/useDatabases"; import { useSidePanel } from "hooks/useSidePanel"; import React, { useEffect, useState } from "react"; import { CollectionCreation } from "Shared/Constants"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import { userContext } from "UserContext"; import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; export interface AddGlobalSecondaryIndexPanelProps { explorer: Explorer; sourceContainer?: Collection; } export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanelProps): JSX.Element => { const { explorer, sourceContainer } = props; const [sourceContainerOptions, setSourceContainerOptions] = useState(); const [selectedSourceContainer, setSelectedSourceContainer] = useState(sourceContainer); const [globalSecondaryIndexId, setGlobalSecondaryIndexId] = useState(); const [definition, setDefinition] = useState(); const [partitionKey, setPartitionKey] = useState(getPartitionKey()); const [subPartitionKeys, setSubPartitionKeys] = useState([]); const [useHashV1, setUseHashV1] = useState(); const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState(); const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState(); const [enableAnalyticalStore, setEnableAnalyticalStore] = useState(); const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState([]); const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState([]); const [vectorPolicyValidated, setVectorPolicyValidated] = useState(true); const [fullTextPolicy, setFullTextPolicy] = useState(FullTextPolicyDefault); const [fullTextIndexes, setFullTextIndexes] = useState([]); const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState(true); const [errorMessage, setErrorMessage] = useState(); const [showErrorDetails, setShowErrorDetails] = useState(); const [isExecuting, setIsExecuting] = useState(); useEffect(() => { const sourceContainerOptions: IDropdownOption[] = []; useDatabases.getState().databases.forEach((database: Database) => { sourceContainerOptions.push({ key: database.rid, text: database.id(), itemType: DropdownMenuItemType.Header, }); database.collections().forEach((collection: Collection) => { const isGlobalSecondaryIndex: boolean = !!collection.materializedViewDefinition(); sourceContainerOptions.push({ key: collection.rid, text: collection.id(), disabled: isGlobalSecondaryIndex, ...(isGlobalSecondaryIndex && { title: "This is a global secondary index.", }), data: collection, }); }); }); setSourceContainerOptions(sourceContainerOptions); }, []); useEffect(() => { scrollToSection("panelContainer"); }, [errorMessage]); let globalSecondaryIndexThroughput: number; let isCostAcknowledged: boolean; const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => { globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue; }; const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => { isCostAcknowledged = isCostAcknowledgedValue; }; const isSelectedSourceContainerSharedThroughput = (): boolean => { if (!selectedSourceContainer) { return false; } return !!selectedSourceContainer.getDatabase().offer(); }; const showCollectionThroughputInput = (): boolean => { if (isServerlessAccount()) { return false; } if (enableDedicatedThroughput) { return true; } return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput(); }; const showVectorSearchParameters = (): boolean => { return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput()); }; const showFullTextSearchParameters = (): boolean => { return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput()); }; const getAnalyticalStorageTtl = (): number => { if (!isSynapseLinkEnabled()) { return undefined; } if (!shouldShowAnalyticalStoreOptions()) { return undefined; } if (enableAnalyticalStore) { // TODO: always default to 90 days once the backend hotfix is deployed return userContext.features.ttl90Days ? Constants.AnalyticalStorageTtl.Days90 : Constants.AnalyticalStorageTtl.Infinite; } return Constants.AnalyticalStorageTtl.Disabled; }; const validateInputs = (): boolean => { if (!selectedSourceContainer) { setErrorMessage("Please select a source container"); return false; } if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { const errorMessage: string = "Please acknowledge the estimated monthly spend."; setErrorMessage(errorMessage); return false; } if (globalSecondaryIndexThroughput > CollectionCreation.MaxRUPerPartition) { setErrorMessage("Unsharded collections support up to 10,000 RUs"); return false; } if (showVectorSearchParameters()) { if (!vectorPolicyValidated) { setErrorMessage("Please fix errors in container vector policy"); return false; } if (!fullTextPolicyValidated) { setErrorMessage("Please fix errors in container full text search policy"); return false; } } return true; }; const submit = async (event?: React.FormEvent): Promise => { event?.preventDefault(); if (!validateInputs()) { return; } const globalSecondaryIdTrimmed: string = globalSecondaryIndexId.trim(); const globalSecondaryIndexDefinition: DataModels.MaterializedViewDefinition = { sourceCollectionId: selectedSourceContainer.id(), definition: definition, }; const partitionKeyTrimmed: string = partitionKey.trim(); const partitionKeyVersion = useHashV1 ? undefined : 2; const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed ? { paths: [ partitionKeyTrimmed, ...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []), ], kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash", version: partitionKeyVersion, } : undefined; const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed; let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy; if (showVectorSearchParameters()) { indexingPolicy.vectorIndexes = vectorIndexingPolicy; vectorEmbeddingPolicyFinal = { vectorEmbeddings: vectorEmbeddingPolicy, }; } if (showFullTextSearchParameters()) { indexingPolicy.fullTextIndexes = fullTextIndexes; } const telemetryData: TelemetryProcessor.TelemetryData = { database: { id: selectedSourceContainer.databaseId, shared: isSelectedSourceContainerSharedThroughput(), }, collection: { id: globalSecondaryIdTrimmed, throughput: globalSecondaryIndexThroughput, isAutoscale: true, partitionKeyPaths, collectionWithDedicatedThroughput: enableDedicatedThroughput, }, subscriptionQuotaId: userContext.quotaId, dataExplorerArea: Constants.Areas.ContextualPane, }; const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData); const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput; const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = { materializedViewId: globalSecondaryIdTrimmed, materializedViewDefinition: globalSecondaryIndexDefinition, databaseId: selectedSourceContainer.databaseId, databaseLevelThroughput: databaseLevelThroughput, ...(!databaseLevelThroughput && { autoPilotMaxThroughput: globalSecondaryIndexThroughput, }), analyticalStorageTtl: getAnalyticalStorageTtl(), indexingPolicy: indexingPolicy, partitionKey: partitionKeyPaths, vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal, fullTextPolicy: fullTextPolicy, }; setIsExecuting(true); try { await createGlobalSecondaryIndex(createGlobalSecondaryIndexParams); await explorer.refreshAllDatabases(); TelemetryProcessor.traceSuccess(Action.CreateGlobalSecondaryIndex, telemetryData, startKey); useSidePanel.getState().closeSidePanel(); } catch (error) { const errorMessage: string = getErrorMessage(error); setErrorMessage(errorMessage); setShowErrorDetails(true); const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) }; TelemetryProcessor.traceFailure(Action.CreateGlobalSecondaryIndex, failureTelemetryData, startKey); } finally { setIsExecuting(false); } }; return (
{errorMessage && ( )}
Source container id setSelectedSourceContainer(options.data as Collection)} /> Global secondary index container id ) => setGlobalSecondaryIndexId(event.target.value)} /> Global secondary index definition Learn more about defining global secondary indexes. } > ) => setDefinition(event.target.value)} /> {shouldShowAnalyticalStoreOptions() && ( )} {showVectorSearchParameters() && ( )} {showFullTextSearchParameters() && ( )}
{isExecuting && } ); };