mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-01 07:11:23 +00:00
Global Secondary Index (#2071)
* add Materialized Views feature flag * fetch MV properties from RP API and capture them in our data models * AddMaterializedViewPanel * undefined check * subpartition keys * Partition Key, Throughput, Unique Keys * All views associated with a container (#2063) and Materialized View Target Container (#2065) Identified Source container and Target container Created tabs in Scale and Settings respectively Changed the Icon of target container * Add MV Panel * format * format * styling * add tests * tests * test files (#2074) Co-authored-by: nishthaAhujaa * fix type error * fix tests * merge conflict * Panel Integration (#2075) * integrated panel * edited header text --------- Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in> Co-authored-by: Asier Isayas <aisayas@microsoft.com> * updated tests (#2077) Co-authored-by: nishthaAhujaa * fix tests * update treeNodeUtil test snap * update settings component test snap * fixed source container in global "New Materialized View" * source container check (#2079) Co-authored-by: nishthaAhujaa * renamed Materialized Views to Global Secondary Index * more renaming * fix import * fix typo * disable materialized views for Fabric * updated input validation --------- Co-authored-by: Asier Isayas <aisayas@microsoft.com> Co-authored-by: Nishtha Ahuja <45535788+nishthaAhujaa@users.noreply.github.com> Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
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,
|
||||
parseUniqueKeys,
|
||||
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 { UniqueKeysComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent";
|
||||
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<IDropdownOption[]>();
|
||||
const [selectedSourceContainer, setSelectedSourceContainer] = useState<Collection>(sourceContainer);
|
||||
const [globalSecondaryIndexId, setGlobalSecondaryIndexId] = useState<string>();
|
||||
const [definition, setDefinition] = useState<string>();
|
||||
const [partitionKey, setPartitionKey] = useState<string>(getPartitionKey());
|
||||
const [subPartitionKeys, setSubPartitionKeys] = useState<string[]>([]);
|
||||
const [useHashV1, setUseHashV1] = useState<boolean>();
|
||||
const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState<boolean>();
|
||||
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>();
|
||||
const [uniqueKeys, setUniqueKeys] = useState<string[]>([]);
|
||||
const [enableAnalyticalStore, setEnableAnalyticalStore] = useState<boolean>();
|
||||
const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState<VectorEmbedding[]>();
|
||||
const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState<VectorIndex[]>();
|
||||
const [vectorPolicyValidated, setVectorPolicyValidated] = useState<boolean>();
|
||||
const [fullTextPolicy, setFullTextPolicy] = useState<FullTextPolicy>(FullTextPolicyDefault);
|
||||
const [fullTextIndexes, setFullTextIndexes] = useState<FullTextIndex[]>();
|
||||
const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState<boolean>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||
|
||||
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 isGlobalSecondaryIndexAutoscale: boolean;
|
||||
let isCostAcknowledged: boolean;
|
||||
|
||||
const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => {
|
||||
globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue;
|
||||
};
|
||||
|
||||
const isGlobalSecondaryIndexAutoscaleOnChange = (isGlobalSecondaryIndexAutoscaleValue: boolean): void => {
|
||||
isGlobalSecondaryIndexAutoscale = isGlobalSecondaryIndexAutoscaleValue;
|
||||
};
|
||||
|
||||
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 = isGlobalSecondaryIndexAutoscale
|
||||
? "Please acknowledge the estimated monthly spend."
|
||||
: "Please acknowledge the estimated daily 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<HTMLFormElement>): Promise<void> => {
|
||||
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 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: globalSecondaryIdTrimmed,
|
||||
throughput: globalSecondaryIndexThroughput,
|
||||
isAutoscale: isGlobalSecondaryIndexAutoscale,
|
||||
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 (isGlobalSecondaryIndexAutoscale) {
|
||||
autoPilotMaxThroughput = globalSecondaryIndexThroughput;
|
||||
} else {
|
||||
offerThroughput = globalSecondaryIndexThroughput;
|
||||
}
|
||||
}
|
||||
|
||||
const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = {
|
||||
materializedViewId: globalSecondaryIdTrimmed,
|
||||
materializedViewDefinition: globalSecondaryIndexDefinition,
|
||||
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 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 (
|
||||
<form className="panelFormWrapper" id="panelGlobalSecondaryIndex" onSubmit={submit}>
|
||||
{errorMessage && (
|
||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Source container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<Dropdown
|
||||
placeholder="Choose source container"
|
||||
options={sourceContainerOptions}
|
||||
defaultSelectedKey={selectedSourceContainer?.rid}
|
||||
styles={chooseSourceContainerStyles()}
|
||||
style={chooseSourceContainerStyle()}
|
||||
onChange={(_, options: IDropdownOption) => setSelectedSourceContainer(options.data as Collection)}
|
||||
/>
|
||||
<Separator className="panelSeparator" />
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Global secondary index container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<input
|
||||
id="globalSecondaryIndexId"
|
||||
type="text"
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., indexbyEmailId`}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={globalSecondaryIndexId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setGlobalSecondaryIndexId(event.target.value)}
|
||||
/>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Global secondary index definition
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||
target="blank"
|
||||
>
|
||||
Learn more about defining global secondary indexes.
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<Icon role="button" iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
<input
|
||||
id="globalSecondaryIndexDefinition"
|
||||
type="text"
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder={"SELECT c.email, c.accountId FROM c"}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={definition || ""}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDefinition(event.target.value)}
|
||||
/>
|
||||
<PartitionKeyComponent
|
||||
{...{ partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 }}
|
||||
/>
|
||||
<ThroughputComponent
|
||||
{...{
|
||||
enableDedicatedThroughput,
|
||||
setEnabledDedicatedThroughput,
|
||||
isSelectedSourceContainerSharedThroughput,
|
||||
showCollectionThroughputInput,
|
||||
globalSecondaryIndexThroughputOnChange,
|
||||
isGlobalSecondaryIndexAutoscaleOnChange,
|
||||
setIsThroughputCapExceeded,
|
||||
isCostAknowledgedOnChange,
|
||||
}}
|
||||
/>
|
||||
<UniqueKeysComponent {...{ uniqueKeys, setUniqueKeys }} />
|
||||
{shouldShowAnalyticalStoreOptions() && (
|
||||
<AnalyticalStoreComponent {...{ explorer, enableAnalyticalStore, setEnableAnalyticalStore }} />
|
||||
)}
|
||||
{showVectorSearchParameters() && (
|
||||
<VectorSearchComponent
|
||||
{...{
|
||||
vectorEmbeddingPolicy,
|
||||
setVectorEmbeddingPolicy,
|
||||
vectorIndexingPolicy,
|
||||
setVectorIndexingPolicy,
|
||||
vectorPolicyValidated,
|
||||
setVectorPolicyValidated,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showFullTextSearchParameters() && (
|
||||
<FullTextSearchComponent
|
||||
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
||||
/>
|
||||
)}
|
||||
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
||||
</Stack>
|
||||
</div>
|
||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
||||
{isExecuting && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user