diff --git a/src/Common/DatabaseAccountUtility.ts b/src/Common/DatabaseAccountUtility.ts index 62b05cf6a..4c7460e81 100644 --- a/src/Common/DatabaseAccountUtility.ts +++ b/src/Common/DatabaseAccountUtility.ts @@ -28,9 +28,5 @@ export function getWorkloadType(): WorkloadType { } export function isMaterializedViewsEnabled() { - return ( - userContext.features.enableMaterializedViews && - userContext.apiType === "SQL" && - userContext.databaseAccount?.properties?.enableMaterializedViews - ); + return userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews; } diff --git a/src/Common/dataAccess/createMaterializedView.ts b/src/Common/dataAccess/createMaterializedView.ts new file mode 100644 index 000000000..467f8fcd8 --- /dev/null +++ b/src/Common/dataAccess/createMaterializedView.ts @@ -0,0 +1,70 @@ +import { constructRpOptions } from "Common/dataAccess/createCollection"; +import { handleError } from "Common/ErrorHandlingUtils"; +import { Collection, CreateMaterializedViewsParams } from "Contracts/DataModels"; +import { userContext } from "UserContext"; +import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources"; +import { + CreateUpdateOptions, + SqlContainerResource, + SqlDatabaseCreateUpdateParameters, +} from "Utils/arm/generatedClients/cosmos/types"; +import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils"; + +export const createMaterializedView = async (params: CreateMaterializedViewsParams): Promise => { + const clearMessage = logConsoleProgress( + `Creating a new materialized view ${params.materializedViewId} for database ${params.databaseId}`, + ); + + const options: CreateUpdateOptions = constructRpOptions(params); + + const resource: SqlContainerResource = { + id: params.materializedViewId, + }; + if (params.materializedViewDefinition) { + resource.materializedViewDefinition = params.materializedViewDefinition; + } + if (params.analyticalStorageTtl) { + resource.analyticalStorageTtl = params.analyticalStorageTtl; + } + if (params.indexingPolicy) { + resource.indexingPolicy = params.indexingPolicy; + } + if (params.partitionKey) { + resource.partitionKey = params.partitionKey; + } + if (params.uniqueKeyPolicy) { + resource.uniqueKeyPolicy = params.uniqueKeyPolicy; + } + if (params.vectorEmbeddingPolicy) { + resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy; + } + if (params.fullTextPolicy) { + resource.fullTextPolicy = params.fullTextPolicy; + } + + const rpPayload: SqlDatabaseCreateUpdateParameters = { + properties: { + resource, + options, + }, + }; + + try { + const createResponse = await createUpdateSqlContainer( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId, + params.materializedViewId, + rpPayload, + ); + logConsoleInfo(`Successfully created materialized view ${params.materializedViewId}`); + + return createResponse && (createResponse.properties.resource as Collection); + } catch (error) { + handleError(error, "CreateMaterializedView", `Error while creating materialized view ${params.materializedViewId}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 6538dc19a..58e412b76 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -234,7 +234,7 @@ export interface MaterializedView { export interface MaterializedViewDefinition { definition: string; sourceCollectionId: string; - sourceCollectionRid: string; + sourceCollectionRid?: string; } export interface PartitionKey { @@ -359,9 +359,7 @@ export interface CreateDatabaseParams { offerThroughput?: number; } -export interface CreateCollectionParams { - createNewDatabase: boolean; - collectionId: string; +export interface CreateCollectionParamsBase { databaseId: string; databaseLevelThroughput: boolean; offerThroughput?: number; @@ -375,6 +373,16 @@ export interface CreateCollectionParams { fullTextPolicy?: FullTextPolicy; } +export interface CreateCollectionParams extends CreateCollectionParamsBase { + createNewDatabase: boolean; + collectionId: string; +} + +export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase { + materializedViewId: string; + materializedViewDefinition: MaterializedViewDefinition; +} + export interface VectorEmbeddingPolicy { vectorEmbeddings: VectorEmbedding[]; } diff --git a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx index 541254d6e..860154cc2 100644 --- a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx @@ -21,17 +21,23 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { configContext, Platform } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; -import { - FullTextPoliciesComponent, - getFullTextLanguageOptions, -} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; +import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { + AllPropertiesIndexed, + AnalyticalStorageContent, + ContainerVectorPolicyTooltipContent, + FullTextPolicyDefault, getPartitionKey, getPartitionKeyName, getPartitionKeyPlaceHolder, getPartitionKeyTooltipText, isFreeTierAccount, + isSynapseLinkEnabled, + parseUniqueKeys, + scrollToSection, + SharedDatabaseDefault, + shouldShowAnalyticalStoreOptions, UniqueKeysHeader, } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; import { useSidePanel } from "hooks/useSidePanel"; @@ -65,40 +71,6 @@ export interface AddCollectionPanelProps { isQuickstart?: boolean; } -const SharedDatabaseDefault: DataModels.IndexingPolicy = { - indexingMode: "consistent", - automatic: true, - includedPaths: [], - excludedPaths: [ - { - path: "/*", - }, - ], -}; - -export const AllPropertiesIndexed: DataModels.IndexingPolicy = { - indexingMode: "consistent", - automatic: true, - includedPaths: [ - { - path: "/*", - indexes: [ - { - kind: "Range", - dataType: "Number", - precision: -1, - }, - { - kind: "Range", - dataType: "String", - precision: -1, - }, - ], - }, - ], - excludedPaths: [], -}; - export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { vectorEmbeddings: [], }; @@ -167,7 +139,7 @@ export class AddCollectionPanel extends React.Component )} - {this.shouldShowAnalyticalStoreOptions() && ( + {shouldShowAnalyticalStoreOptions() && ( - {this.getAnalyticalStorageContent()} + {AnalyticalStorageContent()} @@ -813,7 +785,7 @@ export class AddCollectionPanel extends React.Component - {!this.isSynapseLinkEnabled() && ( + {!isSynapseLinkEnabled() && ( Azure Synapse Link is required for creating an analytical store{" "} @@ -872,9 +844,9 @@ export class AddCollectionPanel extends React.Component { - this.scrollToSection("collapsibleVectorPolicySectionContent"); + scrollToSection("collapsibleVectorPolicySectionContent"); }} - tooltipContent={this.getContainerVectorPolicyTooltipContent()} + tooltipContent={ContainerVectorPolicyTooltipContent()} > @@ -900,7 +872,7 @@ export class AddCollectionPanel extends React.Component { - this.scrollToSection("collapsibleFullTextPolicySectionContent"); + scrollToSection("collapsibleFullTextPolicySectionContent"); }} //TODO: uncomment when learn more text becomes available // tooltipContent={this.getContainerFullTextPolicyTooltipContent()} @@ -928,7 +900,7 @@ export class AddCollectionPanel extends React.Component { TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); - this.scrollToSection("collapsibleAdvancedSectionContent"); + scrollToSection("collapsibleAdvancedSectionContent"); }} > @@ -1142,34 +1114,6 @@ export class AddCollectionPanel extends React.Component - Enable analytical store capability to perform near real-time analytics on your operational data, without - impacting the performance of transactional workloads.{" "} - - Learn more - - - ); - } - - private getContainerVectorPolicyTooltipContent(): JSX.Element { - return ( - - Describe any properties in your data that contain vectors, so that they can be made available for similarity - queries.{" "} - - Learn more - - - ); - } - //TODO: uncomment when learn more text becomes available // private getContainerFullTextPolicyTooltipContent(): JSX.Element { // return ( @@ -1209,39 +1153,6 @@ export class AddCollectionPanel extends React.Component capability.name === Constants.CapabilityNames.EnableStorageAnalytics, - ); - } - private shouldShowVectorSearchParameters() { return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput()); } @@ -1322,11 +1233,11 @@ export class AddCollectionPanel extends React.Component ); } + +export function shouldShowAnalyticalStoreOptions(): boolean { + if (configContext.platform === Platform.Emulator) { + return false; + } + + switch (userContext.apiType) { + case "SQL": + case "Mongo": + return true; + default: + return false; + } +} + +export function AnalyticalStorageContent(): JSX.Element { + return ( + + Enable analytical store capability to perform near real-time analytics on your operational data, without impacting + the performance of transactional workloads.{" "} + + Learn more + + + ); +} + +export function isSynapseLinkEnabled(): boolean { + if (!userContext.databaseAccount) { + return false; + } + + const { properties } = userContext.databaseAccount; + if (!properties) { + return false; + } + + if (properties.enableAnalyticalStorage) { + return true; + } + + return properties.capabilities?.some( + (capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics, + ); +} + +export function scrollToSection(id: string): void { + document.getElementById(id)?.scrollIntoView(); +} + +export function ContainerVectorPolicyTooltipContent(): JSX.Element { + return ( + + Describe any properties in your data that contain vectors, so that they can be made available for similarity + queries.{" "} + + Learn more + + + ); +} + +export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy { + if (uniqueKeys?.length === 0) { + return undefined; + } + + const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] }; + uniqueKeys.forEach((uniqueKey: string) => { + if (uniqueKey) { + const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0); + const trimmedPaths: string[] = validPaths?.map((path) => path.trim()); + if (trimmedPaths?.length > 0) { + if (userContext.apiType === "Mongo") { + trimmedPaths.map((path) => { + const transformedPath = path.split(".").join("/"); + if (transformedPath[0] !== "/") { + return "/" + transformedPath; + } + return transformedPath; + }); + } + uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths }); + } + } + }); + + return uniqueKeyPolicy; +} + +export const SharedDatabaseDefault: DataModels.IndexingPolicy = { + indexingMode: "consistent", + automatic: true, + includedPaths: [], + excludedPaths: [ + { + path: "/*", + }, + ], +}; + +export const FullTextPolicyDefault: DataModels.FullTextPolicy = { + defaultLanguage: getFullTextLanguageOptions()[0].key as never, + fullTextPaths: [], +}; + +export const AllPropertiesIndexed: DataModels.IndexingPolicy = { + indexingMode: "consistent", + automatic: true, + includedPaths: [ + { + path: "/*", + indexes: [ + { + kind: "Range", + dataType: "Number", + precision: -1, + }, + { + kind: "Range", + dataType: "String", + precision: -1, + }, + ], + }, + ], + excludedPaths: [], +}; diff --git a/src/Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent.tsx b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent.tsx new file mode 100644 index 000000000..0f0dfd778 --- /dev/null +++ b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent.tsx @@ -0,0 +1,54 @@ +import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react"; +import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; +import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; + +export interface AddMVAdvancedComponentProps { + useHashV1: boolean; + setUseHashV1: React.Dispatch>; + setSubPartitionKeys: React.Dispatch>; +} +export const AddMVAdvancedComponent = (props: AddMVAdvancedComponentProps): JSX.Element => { + const { useHashV1, setUseHashV1, setSubPartitionKeys } = props; + + const useHashV1CheckboxOnChange = (isChecked: boolean): void => { + setUseHashV1(isChecked); + setSubPartitionKeys([]); + }; + + return ( + { + TelemetryProcessor.traceOpen(Action.ExpandAddMaterializedViewPaneAdvancedSection); + scrollToSection("collapsibleAdvancedSectionContent"); + }} + > + + , isChecked: boolean) => { + useHashV1CheckboxOnChange(isChecked); + }} + /> + + To ensure compatibility with older SDKs, the created + container will use a legacy partitioning scheme that supports partition key values of size only up to 101 + bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "} + + Learn more + + + + + ); +}; diff --git a/src/Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent.tsx b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent.tsx new file mode 100644 index 000000000..9d25eda0e --- /dev/null +++ b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent.tsx @@ -0,0 +1,99 @@ +import { DefaultButton, Link, Stack, Text } from "@fluentui/react"; +import * as Constants from "Common/Constants"; +import Explorer from "Explorer/Explorer"; +import { + AnalyticalStorageContent, + isSynapseLinkEnabled, +} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; +import { getCollectionName } from "Utils/APITypeUtils"; + +export interface AddMVAnalyticalStoreComponentProps { + explorer: Explorer; + enableAnalyticalStore: boolean; + setEnableAnalyticalStore: React.Dispatch>; +} +export const AddMVAnalyticalStoreComponent = (props: AddMVAnalyticalStoreComponentProps): JSX.Element => { + const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props; + + const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => { + if (checked && !enableAnalyticalStore) { + setEnableAnalyticalStore(true); + } + }; + + const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => { + if (checked && enableAnalyticalStore) { + setEnableAnalyticalStore(false); + } + }; + + return ( + + + {AnalyticalStorageContent()} + + + +
+ ) => { + onEnableAnalyticalStoreRadioButtonChange(event.target.checked); + }} + /> + On + + ) => { + onDisableAnalyticalStoreRadioButtonnChange(event.target.checked); + }} + /> + Off +
+
+ + {!isSynapseLinkEnabled() && ( + + + Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}. + Enable Synapse Link for this Cosmos DB account.{" "} + + Learn more + + + explorer.openEnableSynapseLinkDialog()} + style={{ height: 27, width: 80 }} + styles={{ label: { fontSize: 12 } }} + /> + + )} +
+ ); +}; diff --git a/src/Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent.tsx b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent.tsx new file mode 100644 index 000000000..6116dc6c0 --- /dev/null +++ b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent.tsx @@ -0,0 +1,45 @@ +import { Stack } from "@fluentui/react"; +import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels"; +import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; +import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; +import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; + +export interface AddMVFullTextSearchComponentProps { + fullTextPolicy: FullTextPolicy; + setFullTextPolicy: React.Dispatch>; + setFullTextIndexes: React.Dispatch>; + setFullTextPolicyValidated: React.Dispatch>; +} +export const AddMVFullTextSearchComponent = (props: AddMVFullTextSearchComponentProps): JSX.Element => { + const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props; + + return ( + + { + scrollToSection("collapsibleFullTextPolicySectionContent"); + }} + > + + + { + setFullTextPolicy(fullTextPolicy); + setFullTextIndexes(fullTextIndexes); + setFullTextPolicyValidated(fullTextPolicyValidated); + }} + /> + + + + + ); +}; diff --git a/src/Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent.tsx b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent.tsx index 0dd40cf24..9f8e86b6a 100644 --- a/src/Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent.tsx +++ b/src/Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent.tsx @@ -1,5 +1,4 @@ import { Checkbox, Stack } from "@fluentui/react"; -import { Collection } from "Contracts/ViewModels"; import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput"; import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; import { useDatabases } from "Explorer/useDatabases"; @@ -8,9 +7,10 @@ import { getCollectionName } from "Utils/APITypeUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils"; export interface AddMVThroughputComponentProps { - selectedSourceContainer: Collection; enableDedicatedThroughput: boolean; setEnabledDedicatedThroughput: React.Dispatch>; + isSelectedSourceContainerSharedThroughput: () => boolean; + showCollectionThroughputInput: () => boolean; materializedViewThroughputOnChange: (materializedViewThroughputValue: number) => void; isMaterializedViewAutoscaleOnChange: (isMaterializedViewAutoscaleValue: boolean) => void; setIsThroughputCapExceeded: React.Dispatch>; @@ -19,35 +19,16 @@ export interface AddMVThroughputComponentProps { export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): JSX.Element => { const { - selectedSourceContainer, enableDedicatedThroughput, setEnabledDedicatedThroughput, + isSelectedSourceContainerSharedThroughput, + showCollectionThroughputInput, materializedViewThroughputOnChange, isMaterializedViewAutoscaleOnChange, setIsThroughputCapExceeded, isCostAknowledgedOnChange, } = props; - 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(); - }; - return ( {!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && ( @@ -68,7 +49,6 @@ export const AddMVThroughputComponent = (props: AddMVThroughputComponentProps): >; + vectorIndexingPolicy: VectorIndex[]; + setVectorIndexingPolicy: React.Dispatch>; + setVectorPolicyValidated: React.Dispatch>; +} + +export const AddMVVectorSearchComponent = (props: AddMVVectorSearchComponentProps): JSX.Element => { + const { + vectorEmbeddingPolicy, + setVectorEmbeddingPolicy, + vectorIndexingPolicy, + setVectorIndexingPolicy, + setVectorPolicyValidated, + } = props; + + return ( + + { + scrollToSection("collapsibleVectorPolicySectionContent"); + }} + tooltipContent={ContainerVectorPolicyTooltipContent()} + > + + + { + setVectorEmbeddingPolicy(vectorEmbeddingPolicy); + setVectorIndexingPolicy(vectorIndexingPolicy); + setVectorPolicyValidated(vectorPolicyValidated); + }} + /> + + + + + ); +}; diff --git a/src/Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel.tsx b/src/Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel.tsx index 6a4d5daa5..5144102eb 100644 --- a/src/Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel.tsx +++ b/src/Explorer/Panes/AddMaterializedViewPanel/AddMaterializedViewPanel.tsx @@ -10,14 +10,40 @@ import { Text, TooltipHost, } from "@fluentui/react"; +import * as Constants from "Common/Constants"; +import { createMaterializedView } 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 { getPartitionKey } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import { + AllPropertiesIndexed, + FullTextPolicyDefault, + getPartitionKey, + isSynapseLinkEnabled, + parseUniqueKeys, + scrollToSection, + shouldShowAnalyticalStoreOptions, +} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import { AddMVAdvancedComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAdvancedComponent"; +import { AddMVAnalyticalStoreComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVAnalyticalStoreComponent"; +import { AddMVFullTextSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVFullTextSearchComponent"; import { AddMVPartitionKeyComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVPartitionKeyComponent"; import { AddMVThroughputComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVThroughputComponent"; import { AddMVUniqueKeysComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVUniqueKeysComponent"; +import { AddMVVectorSearchComponent } from "Explorer/Panes/AddMaterializedViewPanel/AddMVVectorSearchComponent"; +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"; export interface AddMaterializedViewPanelProps { explorer: Explorer; @@ -27,15 +53,25 @@ export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): const { explorer, sourceContainer } = props; const [sourceContainerOptions, setSourceContainerOptions] = useState(); - const [selectedSourceContainer, setSelectedSourceContainer] = useState(); + const [selectedSourceContainer, setSelectedSourceContainer] = useState(sourceContainer); const [materializedViewId, setMaterializedViewId] = useState(); - const [materializedViewDefinition, setMaterializedViewDefinition] = 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 [uniqueKeys, setUniqueKeys] = useState([]); + const [enableAnalyticalStore, setEnableAnalyticalStore] = useState(); + const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState(); + const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState(); + const [vectorPolicyValidated, setVectorPolicyValidated] = useState(); + const [fullTextPolicy, setFullTextPolicy] = useState(FullTextPolicyDefault); + const [fullTextIndexes, setFullTextIndexes] = useState(); + const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState(); + const [errorMessage, setErrorMessage] = useState(); + const [showErrorDetails, setShowErrorDetails] = useState(); + const [isExecuting, setIsExecuting] = useState(); useEffect(() => { const sourceContainerOptions: IDropdownOption[] = []; @@ -63,6 +99,10 @@ export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): setSourceContainerOptions(sourceContainerOptions); }, []); + useEffect(() => { + scrollToSection("panelContainer"); + }, [errorMessage]); + let materializedViewThroughput: number; let isMaterializedViewAutoscale: boolean; let isCostAcknowledged: boolean; @@ -79,8 +119,199 @@ export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): 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 (materializedViewThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { + const errorMessage = isMaterializedViewAutoscale + ? "Please acknowledge the estimated monthly spend." + : "Please acknowledge the estimated daily spend."; + setErrorMessage(errorMessage); + return false; + } + + if (materializedViewThroughput > 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 materializedViewIdTrimmed: string = materializedViewId.trim(); + + const materializedViewDefinition: DataModels.MaterializedViewDefinition = { + sourceCollectionId: sourceContainer.id(), + definition: definition, + }; + + const partitionKeyTrimmed: string = partitionKey.trim(); + + const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys); + 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: materializedViewIdTrimmed, + throughput: materializedViewThroughput, + isAutoscale: isMaterializedViewAutoscale, + partitionKeyPaths, + uniqueKeyPolicy, + collectionWithDedicatedThroughput: enableDedicatedThroughput, + }, + subscriptionQuotaId: userContext.quotaId, + dataExplorerArea: Constants.Areas.ContextualPane, + }; + + const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData); + const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput; + + let offerThroughput: number; + let autoPilotMaxThroughput: number; + + if (!databaseLevelThroughput) { + if (isMaterializedViewAutoscale) { + autoPilotMaxThroughput = materializedViewThroughput; + } else { + offerThroughput = materializedViewThroughput; + } + } + + const createMaterializedViewParams: DataModels.CreateMaterializedViewsParams = { + materializedViewId: materializedViewIdTrimmed, + materializedViewDefinition: materializedViewDefinition, + databaseId: selectedSourceContainer.databaseId, + databaseLevelThroughput: databaseLevelThroughput, + offerThroughput: offerThroughput, + autoPilotMaxThroughput: autoPilotMaxThroughput, + analyticalStorageTtl: getAnalyticalStorageTtl(), + indexingPolicy: indexingPolicy, + partitionKey: partitionKeyPaths, + uniqueKeyPolicy: uniqueKeyPolicy, + vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal, + fullTextPolicy: fullTextPolicy, + }; + + setIsExecuting(true); + + try { + await createMaterializedView(createMaterializedViewParams); + await explorer.refreshAllDatabases(); + TelemetryProcessor.traceSuccess(Action.CreateMaterializedView, 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.CreateMaterializedView, failureTelemetryData, startKey); + } finally { + setIsExecuting(false); + } + }; + return ( -
+ + {errorMessage && ( + + )}
@@ -146,8 +377,8 @@ export const AddMaterializedViewPanel = (props: AddMaterializedViewPanelProps): placeholder={"SELECT c.email, c.accountId FROM c"} size={40} className="panelTextField" - value={materializedViewDefinition} - onChange={(event: React.ChangeEvent) => setMaterializedViewDefinition(event.target.value)} + value={definition} + onChange={(event: React.ChangeEvent) => setDefinition(event.target.value)} /> + {shouldShowAnalyticalStoreOptions() && ( + + )} + {showVectorSearchParameters() && ( + + )} + {showFullTextSearchParameters() && ( + + )} + + + {isExecuting && ()}
); diff --git a/src/Explorer/QueryCopilot/CopilotCarousel.tsx b/src/Explorer/QueryCopilot/CopilotCarousel.tsx index 3657bfc84..a1273c910 100644 --- a/src/Explorer/QueryCopilot/CopilotCarousel.tsx +++ b/src/Explorer/QueryCopilot/CopilotCarousel.tsx @@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection"; import * as DataModels from "Contracts/DataModels"; import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator"; import Explorer from "Explorer/Explorer"; -import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanel"; +import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; import { PromptCard } from "Explorer/QueryCopilot/PromptCard"; import { useDatabases } from "Explorer/useDatabases"; import { useCarousel } from "hooks/useCarousel"; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 908a93e28..175c4bcff 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -39,7 +39,6 @@ export type Features = { readonly copilotChatFixedMonacoEditorHeight: boolean; readonly enablePriorityBasedExecution: boolean; readonly disableConnectionStringLogin: boolean; - readonly enableMaterializedViews: boolean; // can be set via both flight and feature flag autoscaleDefault: boolean; @@ -111,7 +110,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"), - enableMaterializedViews: "true" === get("enablematerializedviews"), }; } diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 1fae132ad..4c8f31c61 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -2,6 +2,7 @@ export enum Action { CollapseTreeNode, CreateCollection, + CreateMaterializedView, CreateDocument, CreateStoredProcedure, CreateTrigger, @@ -119,6 +120,7 @@ export enum Action { NotebooksGalleryPublishedCount, SelfServe, ExpandAddCollectionPaneAdvancedSection, + ExpandAddMaterializedViewPaneAdvancedSection, SchemaAnalyzerClickAnalyze, SelfServeComponent, LaunchQuickstart,