mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-30 06:11:38 +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,28 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import {
|
||||
AddGlobalSecondaryIndexPanel,
|
||||
AddGlobalSecondaryIndexPanelProps,
|
||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
||||
import React, { Component } from "react";
|
||||
|
||||
const props: AddGlobalSecondaryIndexPanelProps = {
|
||||
explorer: new Explorer(),
|
||||
};
|
||||
|
||||
describe("AddGlobalSecondaryIndexPanel", () => {
|
||||
it("render default panel", () => {
|
||||
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
|
||||
<AddGlobalSecondaryIndexPanel {...props} />,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render form", () => {
|
||||
const wrapper: ShallowWrapper<AddGlobalSecondaryIndexPanelProps, object, Component> = shallow(
|
||||
<AddGlobalSecondaryIndexPanel {...props} />,
|
||||
);
|
||||
const form = wrapper.find("form").first();
|
||||
expect(form).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function chooseSourceContainerStyles(): IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> {
|
||||
return {
|
||||
title: { height: 27, lineHeight: 27 },
|
||||
dropdownItem: { fontSize: 12 },
|
||||
dropdownItemDisabled: { fontSize: 12 },
|
||||
dropdownItemSelected: { fontSize: 12 },
|
||||
};
|
||||
}
|
||||
|
||||
export function chooseSourceContainerStyle(): CSSProperties {
|
||||
return { width: 300, fontSize: 12 };
|
||||
}
|
||||
@@ -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 AdvancedComponentProps {
|
||||
useHashV1: boolean;
|
||||
setUseHashV1: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
export const AdvancedComponent = (props: AdvancedComponentProps): JSX.Element => {
|
||||
const { useHashV1, setUseHashV1, setSubPartitionKeys } = props;
|
||||
|
||||
const useHashV1CheckboxOnChange = (isChecked: boolean): void => {
|
||||
setUseHashV1(isChecked);
|
||||
setSubPartitionKeys([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddGlobalSecondaryIndexPaneAdvancedSection);
|
||||
scrollToSection("collapsibleAdvancedSectionContent");
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
|
||||
<Checkbox
|
||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||
checked={useHashV1}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
|
||||
useHashV1CheckboxOnChange(isChecked);
|
||||
}}
|
||||
/>
|
||||
<Text variant="small">
|
||||
<Icon iconName="InfoSolid" className="removeIcon" /> 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.{" "}
|
||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
);
|
||||
};
|
||||
@@ -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 AnalyticalStoreComponentProps {
|
||||
explorer: Explorer;
|
||||
enableAnalyticalStore: boolean;
|
||||
setEnableAnalyticalStore: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
export const AnalyticalStoreComponent = (props: AnalyticalStoreComponentProps): 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 (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{AnalyticalStorageContent()}
|
||||
</Text>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-checked={enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="enableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onEnableAnalyticalStoreRadioButtonChange(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">On</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-checked={!enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="disableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onDisableAnalyticalStoreRadioButtonnChange(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!isSynapseLinkEnabled() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small">
|
||||
Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}.
|
||||
Enable Synapse Link for this Cosmos DB account.{" "}
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||
className="capacitycalculator-link"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
<DefaultButton
|
||||
text="Enable"
|
||||
onClick={() => explorer.openEnableSynapseLinkDialog()}
|
||||
style={{ height: 27, width: 80 }}
|
||||
styles={{ label: { fontSize: 12 } }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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 FullTextSearchComponentProps {
|
||||
fullTextPolicy: FullTextPolicy;
|
||||
setFullTextPolicy: React.Dispatch<React.SetStateAction<FullTextPolicy>>;
|
||||
setFullTextIndexes: React.Dispatch<React.SetStateAction<FullTextIndex[]>>;
|
||||
setFullTextPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
export const FullTextSearchComponent = (props: FullTextSearchComponentProps): JSX.Element => {
|
||||
const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Full Text Search Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
}}
|
||||
>
|
||||
<Stack id="collapsibleFullTextPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<FullTextPoliciesComponent
|
||||
fullTextPolicy={fullTextPolicy}
|
||||
onFullTextPathChange={(
|
||||
fullTextPolicy: FullTextPolicy,
|
||||
fullTextIndexes: FullTextIndex[],
|
||||
fullTextPolicyValidated: boolean,
|
||||
) => {
|
||||
setFullTextPolicy(fullTextPolicy);
|
||||
setFullTextIndexes(fullTextIndexes);
|
||||
setFullTextPolicyValidated(fullTextPolicyValidated);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import {
|
||||
getPartitionKeyName,
|
||||
getPartitionKeyPlaceHolder,
|
||||
getPartitionKeyTooltipText,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
|
||||
export interface PartitionKeyComponentProps {
|
||||
partitionKey?: string;
|
||||
setPartitionKey: React.Dispatch<React.SetStateAction<string>>;
|
||||
subPartitionKeys: string[];
|
||||
setSubPartitionKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
useHashV1: boolean;
|
||||
}
|
||||
|
||||
export const PartitionKeyComponent = (props: PartitionKeyComponentProps): JSX.Element => {
|
||||
const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props;
|
||||
|
||||
const partitionKeyValueOnChange = (value: string): void => {
|
||||
if (!partitionKey && !value.startsWith("/")) {
|
||||
setPartitionKey("/" + value);
|
||||
} else {
|
||||
setPartitionKey(value);
|
||||
}
|
||||
};
|
||||
|
||||
const subPartitionKeysValueOnChange = (value: string, index: number): void => {
|
||||
const updatedSubPartitionKeys: string[] = [...subPartitionKeys];
|
||||
if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) {
|
||||
updatedSubPartitionKeys[index] = "/" + value.trim();
|
||||
} else {
|
||||
updatedSubPartitionKeys[index] = value.trim();
|
||||
}
|
||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Partition key
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={getPartitionKeyTooltipText()}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="addGlobalSecondaryIndex-partitionKeyValue"
|
||||
aria-required
|
||||
required
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
placeholder={getPartitionKeyPlaceHolder()}
|
||||
aria-label={getPartitionKeyName()}
|
||||
pattern=".*"
|
||||
value={partitionKey}
|
||||
style={{ marginBottom: 8 }}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
partitionKeyValueOnChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
{subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${subPartitionKeyIndex}`} horizontal>
|
||||
<div
|
||||
style={{
|
||||
width: "20px",
|
||||
border: "solid",
|
||||
borderWidth: "0px 0px 1px 1px",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
></div>
|
||||
<input
|
||||
type="text"
|
||||
id="addGlobalSecondaryIndex-partitionKeyValue"
|
||||
key={`addGlobalSecondaryIndex-partitionKeyValue_${subPartitionKeyIndex}`}
|
||||
aria-required
|
||||
required
|
||||
size={40}
|
||||
tabIndex={subPartitionKeyIndex > 0 ? 1 : 0}
|
||||
className="panelTextField"
|
||||
autoComplete="off"
|
||||
placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)}
|
||||
aria-label={getPartitionKeyName()}
|
||||
pattern={".*"}
|
||||
title={""}
|
||||
value={subPartitionKey}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27 }}
|
||||
onClick={() => {
|
||||
const updatedSubPartitionKeys = subPartitionKeys.filter(
|
||||
(_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove,
|
||||
);
|
||||
setSubPartitionKeys(updatedSubPartitionKeys);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
hidden={useHashV1}
|
||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
</DefaultButton>
|
||||
{subPartitionKeys.length > 0 && (
|
||||
<Text variant="small">
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to partition your
|
||||
data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview
|
||||
JavaScript V3 SDK.{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Checkbox, Stack } from "@fluentui/react";
|
||||
import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput";
|
||||
import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React from "react";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
|
||||
export interface ThroughputComponentProps {
|
||||
enableDedicatedThroughput: boolean;
|
||||
setEnabledDedicatedThroughput: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isSelectedSourceContainerSharedThroughput: () => boolean;
|
||||
showCollectionThroughputInput: () => boolean;
|
||||
globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void;
|
||||
isGlobalSecondaryIndexAutoscaleOnChange: (isGlobalSecondaryIndexAutoscaleValue: boolean) => void;
|
||||
setIsThroughputCapExceeded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void;
|
||||
}
|
||||
|
||||
export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Element => {
|
||||
const {
|
||||
enableDedicatedThroughput,
|
||||
setEnabledDedicatedThroughput,
|
||||
isSelectedSourceContainerSharedThroughput,
|
||||
showCollectionThroughputInput,
|
||||
globalSecondaryIndexThroughputOnChange,
|
||||
isGlobalSecondaryIndexAutoscaleOnChange,
|
||||
setIsThroughputCapExceeded,
|
||||
isCostAknowledgedOnChange,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Checkbox
|
||||
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
||||
checked={enableDedicatedThroughput}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(_, isChecked: boolean) => setEnabledDedicatedThroughput(isChecked)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{showCollectionThroughputInput() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded={false}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isQuickstart={false}
|
||||
setThroughputValue={(throughput: number) => {
|
||||
globalSecondaryIndexThroughputOnChange(throughput);
|
||||
}}
|
||||
setIsAutoscale={(isAutoscale: boolean) => {
|
||||
isGlobalSecondaryIndexAutoscaleOnChange(isAutoscale);
|
||||
}}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => {
|
||||
setIsThroughputCapExceeded(isThroughputCapExceeded);
|
||||
}}
|
||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
||||
isCostAknowledgedOnChange(isAcknowledged);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ActionButton, IconButton, Stack } from "@fluentui/react";
|
||||
import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export interface UniqueKeysComponentProps {
|
||||
uniqueKeys: string[];
|
||||
setUniqueKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
export const UniqueKeysComponent = (props: UniqueKeysComponentProps): JSX.Element => {
|
||||
const { uniqueKeys, setUniqueKeys } = props;
|
||||
|
||||
const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => {
|
||||
const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => {
|
||||
if (uniqueKeyToReplaceIndex === uniqueKeyIndex) {
|
||||
return value;
|
||||
}
|
||||
return uniqueKey;
|
||||
});
|
||||
setUniqueKeys(updatedUniqueKeys);
|
||||
};
|
||||
|
||||
const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => {
|
||||
const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex);
|
||||
setUniqueKeys(updatedUniqueKeys);
|
||||
};
|
||||
|
||||
const addUniqueKeyOnClick = (): void => {
|
||||
setUniqueKeys([...uniqueKeys, ""]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{UniqueKeysHeader()}
|
||||
|
||||
{uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey-${uniqueKeyIndex}`} horizontal>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder={
|
||||
userContext.apiType === "Mongo"
|
||||
? "Comma separated paths e.g. firstName,address.zipCode"
|
||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
||||
}
|
||||
className="panelTextField"
|
||||
autoFocus
|
||||
value={uniqueKey}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27 }}
|
||||
onClick={() => {
|
||||
deleteUniqueKeyOnClick(uniqueKeyIndex);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
|
||||
<ActionButton
|
||||
iconProps={{ iconName: "Add" }}
|
||||
styles={{ root: { padding: 0 }, label: { fontSize: 12 } }}
|
||||
onClick={() => {
|
||||
addUniqueKeyOnClick();
|
||||
}}
|
||||
>
|
||||
Add unique key
|
||||
</ActionButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import {
|
||||
ContainerVectorPolicyTooltipContent,
|
||||
scrollToSection,
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import React from "react";
|
||||
|
||||
export interface VectorSearchComponentProps {
|
||||
vectorEmbeddingPolicy: VectorEmbedding[];
|
||||
setVectorEmbeddingPolicy: React.Dispatch<React.SetStateAction<VectorEmbedding[]>>;
|
||||
vectorIndexingPolicy: VectorIndex[];
|
||||
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
||||
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
|
||||
const {
|
||||
vectorEmbeddingPolicy,
|
||||
setVectorEmbeddingPolicy,
|
||||
vectorIndexingPolicy,
|
||||
setVectorIndexingPolicy,
|
||||
setVectorPolicyValidated,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Vector Policy"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
}}
|
||||
tooltipContent={ContainerVectorPolicyTooltipContent()}
|
||||
>
|
||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||
<VectorEmbeddingPoliciesComponent
|
||||
vectorEmbeddings={vectorEmbeddingPolicy}
|
||||
vectorIndexes={vectorIndexingPolicy}
|
||||
onVectorEmbeddingChange={(
|
||||
vectorEmbeddingPolicy: VectorEmbedding[],
|
||||
vectorIndexingPolicy: VectorIndex[],
|
||||
vectorPolicyValidated: boolean,
|
||||
) => {
|
||||
setVectorEmbeddingPolicy(vectorEmbeddingPolicy);
|
||||
setVectorIndexingPolicy(vectorIndexingPolicy);
|
||||
setVectorPolicyValidated(vectorPolicyValidated);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
||||
<form
|
||||
className="panelFormWrapper"
|
||||
id="panelGlobalSecondaryIndex"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="panelMainContent"
|
||||
>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Source container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<Dropdown
|
||||
onChange={[Function]}
|
||||
placeholder="Choose source container"
|
||||
style={
|
||||
{
|
||||
"fontSize": 12,
|
||||
"width": 300,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
{
|
||||
"dropdownItem": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"dropdownItemDisabled": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"dropdownItemSelected": {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"title": {
|
||||
"height": 27,
|
||||
"lineHeight": 27,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Global secondary index container id
|
||||
</Text>
|
||||
</Stack>
|
||||
<input
|
||||
aria-required={true}
|
||||
autoComplete="off"
|
||||
className="panelTextField"
|
||||
id="globalSecondaryIndexId"
|
||||
onChange={[Function]}
|
||||
pattern="[^/?#\\\\]*[^/?# \\\\]"
|
||||
placeholder="e.g., indexbyEmailId"
|
||||
required={true}
|
||||
size={40}
|
||||
title="May not end with space nor contain characters '\\' '/' '#' '?'"
|
||||
type="text"
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
>
|
||||
*
|
||||
</span>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Global secondary index definition
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content={
|
||||
<StyledLinkBase
|
||||
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.
|
||||
</StyledLinkBase>
|
||||
}
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
<input
|
||||
aria-required={true}
|
||||
autoComplete="off"
|
||||
className="panelTextField"
|
||||
id="globalSecondaryIndexDefinition"
|
||||
onChange={[Function]}
|
||||
placeholder="SELECT c.email, c.accountId FROM c"
|
||||
required={true}
|
||||
size={40}
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<PartitionKeyComponent
|
||||
partitionKey=""
|
||||
setPartitionKey={[Function]}
|
||||
setSubPartitionKeys={[Function]}
|
||||
subPartitionKeys={[]}
|
||||
/>
|
||||
<ThroughputComponent
|
||||
globalSecondaryIndexThroughputOnChange={[Function]}
|
||||
isCostAknowledgedOnChange={[Function]}
|
||||
isGlobalSecondaryIndexAutoscaleOnChange={[Function]}
|
||||
isSelectedSourceContainerSharedThroughput={[Function]}
|
||||
setEnabledDedicatedThroughput={[Function]}
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
showCollectionThroughputInput={[Function]}
|
||||
/>
|
||||
<UniqueKeysComponent
|
||||
setUniqueKeys={[Function]}
|
||||
uniqueKeys={[]}
|
||||
/>
|
||||
<AnalyticalStoreComponent
|
||||
explorer={
|
||||
Explorer {
|
||||
"_isInitializingNotebooks": false,
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isTabsContentExpanded": [Function],
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
"retries": 3,
|
||||
},
|
||||
},
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
},
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
setEnableAnalyticalStore={[Function]}
|
||||
/>
|
||||
<AdvancedComponent
|
||||
setSubPartitionKeys={[Function]}
|
||||
setUseHashV1={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<PanelFooterComponent
|
||||
buttonLabel="OK"
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
Reference in New Issue
Block a user