mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-26 12:21:23 +00:00
* Fix minor issues * add back preview tag * bring back query explanation and remove prompt in editor
399 lines
16 KiB
TypeScript
399 lines
16 KiB
TypeScript
import {
|
||
DefaultButton,
|
||
DirectionalHint,
|
||
Dropdown,
|
||
IDropdownOption,
|
||
Icon,
|
||
IconButton,
|
||
Link,
|
||
MessageBar,
|
||
Stack,
|
||
Text,
|
||
TooltipHost,
|
||
} from "@fluentui/react";
|
||
import * as Constants from "Common/Constants";
|
||
import { handleError } from "Common/ErrorHandlingUtils";
|
||
import { createCollection } from "Common/dataAccess/createCollection";
|
||
import { DataTransferParams, initiateDataTransfer } from "Common/dataAccess/dataTransfers";
|
||
import * as DataModels from "Contracts/DataModels";
|
||
import * as ViewModels from "Contracts/ViewModels";
|
||
import {
|
||
getPartitionKeyName,
|
||
getPartitionKeyPlaceHolder,
|
||
getPartitionKeySubtext,
|
||
getPartitionKeyTooltipText,
|
||
} from "Explorer/Controls/Settings/SettingsUtils";
|
||
import Explorer from "Explorer/Explorer";
|
||
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
||
import { useDatabases } from "Explorer/useDatabases";
|
||
import { userContext } from "UserContext";
|
||
import { getCollectionName } from "Utils/APITypeUtils";
|
||
import { useSidePanel } from "hooks/useSidePanel";
|
||
import * as React from "react";
|
||
|
||
export interface ChangePartitionKeyPaneProps {
|
||
sourceDatabase: ViewModels.Database;
|
||
sourceCollection: ViewModels.Collection;
|
||
explorer: Explorer;
|
||
onClose: () => Promise<void>;
|
||
}
|
||
|
||
export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||
sourceDatabase,
|
||
sourceCollection,
|
||
explorer,
|
||
onClose,
|
||
}) => {
|
||
const [targetCollectionId, setTargetCollectionId] = React.useState<string>();
|
||
const [createNewContainer, setCreateNewContainer] = React.useState<boolean>(true);
|
||
const [formError, setFormError] = React.useState<string>();
|
||
const [isExecuting, setIsExecuting] = React.useState<boolean>(false);
|
||
const [subPartitionKeys, setSubPartitionKeys] = React.useState<string[]>([]);
|
||
const [partitionKey, setPartitionKey] = React.useState<string>();
|
||
|
||
const getCollectionOptions = (): IDropdownOption[] => {
|
||
return sourceDatabase
|
||
.collections()
|
||
.filter((collection) => collection.id !== sourceCollection.id)
|
||
.map((collection) => ({
|
||
key: collection.id(),
|
||
text: collection.id(),
|
||
}));
|
||
};
|
||
|
||
const submit = async () => {
|
||
if (!validateInputs()) {
|
||
return;
|
||
}
|
||
setIsExecuting(true);
|
||
try {
|
||
createNewContainer && (await createContainer());
|
||
await createDataTransferJob();
|
||
await onClose();
|
||
} catch (error) {
|
||
handleError(error, "ChangePartitionKey", "Failed to start data transfer job");
|
||
}
|
||
setIsExecuting(false);
|
||
useSidePanel.getState().closeSidePanel();
|
||
};
|
||
|
||
const validateInputs = (): boolean => {
|
||
if (!createNewContainer && !targetCollectionId) {
|
||
setFormError("Choose an existing container");
|
||
return false;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
const createDataTransferJob = async () => {
|
||
const jobName = `Portal_${targetCollectionId}_${Math.floor(Date.now() / 1000)}`;
|
||
const dataTransferParams: DataTransferParams = {
|
||
jobName,
|
||
apiType: userContext.apiType,
|
||
subscriptionId: userContext.subscriptionId,
|
||
resourceGroupName: userContext.resourceGroup,
|
||
accountName: userContext.databaseAccount.name,
|
||
sourceDatabaseName: sourceDatabase.id(),
|
||
sourceCollectionName: sourceCollection.id(),
|
||
targetDatabaseName: sourceDatabase.id(),
|
||
targetCollectionName: targetCollectionId,
|
||
};
|
||
await initiateDataTransfer(dataTransferParams);
|
||
};
|
||
|
||
const createContainer = async () => {
|
||
const partitionKeyString = partitionKey.trim();
|
||
const partitionKeyData: DataModels.PartitionKey = partitionKeyString
|
||
? {
|
||
paths: [partitionKeyString, ...(subPartitionKeys.length > 0 ? subPartitionKeys : [])],
|
||
kind: subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||
version: 2,
|
||
}
|
||
: undefined;
|
||
|
||
const createCollectionParams: DataModels.CreateCollectionParams = {
|
||
createNewDatabase: false,
|
||
collectionId: targetCollectionId,
|
||
databaseId: sourceDatabase.id(),
|
||
databaseLevelThroughput: isSelectedDatabaseSharedThroughput(),
|
||
offerThroughput: sourceCollection.offer()?.manualThroughput,
|
||
autoPilotMaxThroughput: sourceCollection.offer()?.autoscaleMaxThroughput,
|
||
partitionKey: partitionKeyData,
|
||
};
|
||
await createCollection(createCollectionParams);
|
||
await explorer.refreshAllDatabases();
|
||
};
|
||
|
||
const isSelectedDatabaseSharedThroughput = (): boolean => {
|
||
const selectedDatabase = useDatabases
|
||
.getState()
|
||
.databases?.find((database) => database.id() === sourceDatabase.id());
|
||
return !!selectedDatabase?.offer();
|
||
};
|
||
|
||
return (
|
||
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK">
|
||
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
|
||
<Text variant="small">
|
||
When changing a container’s partition key, you will need to create a destination container with the correct
|
||
partition key. You may also select an existing destination container.
|
||
<Link
|
||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
|
||
target="_blank"
|
||
underline
|
||
>
|
||
Learn more
|
||
</Link>
|
||
</Text>
|
||
<Stack>
|
||
<Stack horizontal>
|
||
<span className="mandatoryStar">* </span>
|
||
<Text className="panelTextBold" variant="small">
|
||
Database id
|
||
</Text>
|
||
<TooltipHost
|
||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||
true,
|
||
).toLocaleLowerCase()}.`}
|
||
>
|
||
<Icon
|
||
iconName="Info"
|
||
className="panelInfoIcon"
|
||
tabIndex={0}
|
||
aria-label={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||
true,
|
||
).toLocaleLowerCase()}.`}
|
||
/>
|
||
</TooltipHost>
|
||
</Stack>
|
||
<Dropdown
|
||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||
style={{ width: 300, fontSize: 12 }}
|
||
options={[]}
|
||
placeholder={sourceDatabase.id()}
|
||
responsiveMode={999}
|
||
disabled={true}
|
||
/>
|
||
</Stack>
|
||
<Stack className="panelGroupSpacing" horizontal verticalAlign="center">
|
||
<div role="radiogroup">
|
||
<input
|
||
className="panelRadioBtn"
|
||
checked={createNewContainer}
|
||
aria-label="Create new container"
|
||
aria-checked={createNewContainer}
|
||
name="containerType"
|
||
type="radio"
|
||
role="radio"
|
||
id="containerCreateNew"
|
||
tabIndex={0}
|
||
onChange={() => setCreateNewContainer(true)}
|
||
/>
|
||
<span className="panelRadioBtnLabel">New container</span>
|
||
|
||
<input
|
||
className="panelRadioBtn"
|
||
checked={!createNewContainer}
|
||
aria-label="Use existing container"
|
||
aria-checked={!createNewContainer}
|
||
name="containerType"
|
||
type="radio"
|
||
role="radio"
|
||
tabIndex={0}
|
||
onChange={() => setCreateNewContainer(false)}
|
||
/>
|
||
<span className="panelRadioBtnLabel">Existing container</span>
|
||
</div>
|
||
</Stack>
|
||
{createNewContainer ? (
|
||
<Stack>
|
||
<MessageBar>All configurations except for unique keys will be copied from the source container</MessageBar>
|
||
<Stack className="panelGroupSpacing">
|
||
<Stack horizontal>
|
||
<span className="mandatoryStar">* </span>
|
||
<Text className="panelTextBold" variant="small">
|
||
{`${getCollectionName()} id`}
|
||
</Text>
|
||
<TooltipHost
|
||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||
>
|
||
<Icon
|
||
role="button"
|
||
iconName="Info"
|
||
className="panelInfoIcon"
|
||
tabIndex={0}
|
||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||
/>
|
||
</TooltipHost>
|
||
</Stack>
|
||
<input
|
||
name="collectionId"
|
||
id="collectionId"
|
||
type="text"
|
||
aria-required
|
||
required
|
||
autoComplete="off"
|
||
pattern="[^/?#\\]*[^/?# \\]"
|
||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||
placeholder={`e.g., ${getCollectionName()}1`}
|
||
size={40}
|
||
className="panelTextField"
|
||
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
||
value={targetCollectionId}
|
||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
|
||
/>
|
||
</Stack>
|
||
<Stack tokens={{ childrenGap: 10 }}>
|
||
<Stack horizontal>
|
||
<span className="mandatoryStar">* </span>
|
||
<Text className="panelTextBold" variant="small">
|
||
{getPartitionKeyName(userContext.apiType)}
|
||
</Text>
|
||
<TooltipHost
|
||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||
content={getPartitionKeyTooltipText(userContext.apiType)}
|
||
>
|
||
<Icon
|
||
iconName="Info"
|
||
className="panelInfoIcon"
|
||
tabIndex={0}
|
||
aria-label={getPartitionKeyTooltipText(userContext.apiType)}
|
||
/>
|
||
</TooltipHost>
|
||
</Stack>
|
||
|
||
<Text variant="small" aria-label="pkDescription">
|
||
{getPartitionKeySubtext(userContext.features.partitionKeyDefault, userContext.apiType)}
|
||
</Text>
|
||
|
||
<input
|
||
type="text"
|
||
id="addCollection-partitionKeyValue"
|
||
aria-required
|
||
required
|
||
size={40}
|
||
className="panelTextField"
|
||
placeholder={getPartitionKeyPlaceHolder(userContext.apiType)}
|
||
aria-label={getPartitionKeyName(userContext.apiType)}
|
||
pattern={".*"}
|
||
title={""}
|
||
value={partitionKey}
|
||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (!partitionKey && !event.target.value.startsWith("/")) {
|
||
setPartitionKey("/" + event.target.value);
|
||
} else {
|
||
setPartitionKey(event.target.value);
|
||
}
|
||
}}
|
||
/>
|
||
{subPartitionKeys.map((subPartitionKey: string, index: number) => {
|
||
return (
|
||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
|
||
<div
|
||
style={{
|
||
width: "20px",
|
||
border: "solid",
|
||
borderWidth: "0px 0px 1px 1px",
|
||
marginRight: "5px",
|
||
}}
|
||
></div>
|
||
<input
|
||
type="text"
|
||
id="addCollection-partitionKeyValue"
|
||
key={`addCollection-partitionKeyValue_${index}`}
|
||
aria-required
|
||
required
|
||
size={40}
|
||
tabIndex={index > 0 ? 1 : 0}
|
||
className="panelTextField"
|
||
autoComplete="off"
|
||
placeholder={getPartitionKeyPlaceHolder(userContext.apiType, index)}
|
||
aria-label={getPartitionKeyName(userContext.apiType)}
|
||
pattern={".*"}
|
||
title={""}
|
||
value={subPartitionKey}
|
||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const keys = [...subPartitionKeys];
|
||
if (!keys[index] && !event.target.value.startsWith("/")) {
|
||
keys[index] = "/" + event.target.value.trim();
|
||
setSubPartitionKeys(keys);
|
||
} else {
|
||
keys[index] = event.target.value.trim();
|
||
setSubPartitionKeys(keys);
|
||
}
|
||
}}
|
||
/>
|
||
<IconButton
|
||
iconProps={{ iconName: "Delete" }}
|
||
style={{ height: 27 }}
|
||
onClick={() => {
|
||
const keys = subPartitionKeys.filter((uniqueKey, j) => index !== j);
|
||
setSubPartitionKeys(keys);
|
||
}}
|
||
/>
|
||
</Stack>
|
||
);
|
||
})}
|
||
<Stack className="panelGroupSpacing">
|
||
<DefaultButton
|
||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||
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>
|
||
</Stack>
|
||
) : (
|
||
<Stack>
|
||
<Stack horizontal>
|
||
<span className="mandatoryStar">* </span>
|
||
<Text className="panelTextBold" variant="small">
|
||
{`${getCollectionName()}`}
|
||
</Text>
|
||
<TooltipHost
|
||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||
>
|
||
<Icon
|
||
role="button"
|
||
iconName="Info"
|
||
className="panelInfoIcon"
|
||
tabIndex={0}
|
||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||
/>
|
||
</TooltipHost>
|
||
</Stack>
|
||
|
||
<Dropdown
|
||
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
|
||
style={{ width: 300, fontSize: 12 }}
|
||
placeholder="Choose an existing container"
|
||
options={getCollectionOptions()}
|
||
onChange={(event: React.FormEvent<HTMLDivElement>, collection: IDropdownOption) => {
|
||
setTargetCollectionId(collection.key as string);
|
||
setFormError("");
|
||
}}
|
||
defaultSelectedKey={targetCollectionId}
|
||
responsiveMode={999}
|
||
/>
|
||
</Stack>
|
||
)}
|
||
</Stack>
|
||
</RightPaneForm>
|
||
);
|
||
};
|