Files
cosmos-explorer/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx
sunghyunkang1111 f723b4746d Localizations batch 5 and import update (#2422)
* Locations import update

* Fix formatting

* Fix import paths
2026-03-11 13:51:19 -05:00

347 lines
14 KiB
TypeScript

import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import { Keys, t } from "Localization";
import React, { FunctionComponent, useState } from "react";
import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import { useDatabases } from "../../useDatabases";
import { getTextFieldStyles } from "../PanelStyles";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface CassandraAddCollectionPaneProps {
explorer: Explorer;
cassandraApiClient: CassandraAPIDataClient;
}
export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectionPaneProps> = ({
explorer: container,
cassandraApiClient,
}: CassandraAddCollectionPaneProps) => {
let newKeySpaceThroughput: number;
let isNewKeySpaceAutoscale: boolean;
let tableThroughput: number;
let isTableAutoscale: boolean;
let isCostAcknowledged: boolean;
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [newKeyspaceId, setNewKeyspaceId] = useState<string>("");
const [existingKeyspaceId, setExistingKeyspaceId] = useState<string>("");
const [tableId, setTableId] = useState<string>("");
const [userTableQuery, setUserTableQuery] = useState<string>(
"(userid int, name text, email text, PRIMARY KEY (userid))",
);
const [isKeyspaceShared, setIsKeyspaceShared] = useState<boolean>(false);
const [keyspaceCreateNew, setKeyspaceCreateNew] = useState<boolean>(true);
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>("");
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const addCollectionPaneOpenMessage = {
collection: {
id: tableId,
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: newKeySpaceThroughput || tableThroughput,
partitionKey: "",
databaseId: keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId,
},
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: newKeySpaceThroughput || tableThroughput,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const onSubmit = async () => {
const throughput = keyspaceCreateNew ? newKeySpaceThroughput : tableThroughput;
const keyspaceId = keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId;
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage =
isNewKeySpaceAutoscale || isTableAutoscale
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
setFormError(errorMessage);
return;
}
setIsExecuting(true);
const autoPilotCommand = `cosmosdb_autoscale_max_throughput`;
const createKeyspaceQueryPrefix = `CREATE KEYSPACE ${keyspaceId.trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`;
const createKeyspaceQuery: string = isKeyspaceShared
? isNewKeySpaceAutoscale
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${newKeySpaceThroughput};`
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${newKeySpaceThroughput};`
: `${createKeyspaceQueryPrefix};`;
let tableQuery: string;
const createTableQueryPrefix = `CREATE TABLE ${keyspaceId}.${tableId.trim()} ${userTableQuery}`;
if (tableThroughput) {
if (isTableAutoscale) {
tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${tableThroughput};`;
} else {
tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${tableThroughput};`;
}
} else {
tableQuery = `${createTableQueryPrefix};`;
}
const addCollectionPaneStartMessage = {
...addCollectionPaneOpenMessage,
collection: {
...addCollectionPaneOpenMessage.collection,
hasDedicatedThroughput: dedicateTableThroughput,
},
isKeyspaceShared,
keyspaceCreateNew,
createKeyspaceQuery,
createTableQuery: tableQuery,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
try {
if (keyspaceCreateNew) {
await cassandraApiClient.createTableAndKeyspace(
userContext?.databaseAccount?.properties?.cassandraEndpoint,
userContext?.databaseAccount?.id,
container,
tableQuery,
createKeyspaceQuery,
);
} else {
await cassandraApiClient.createTableAndKeyspace(
userContext?.databaseAccount?.properties?.cassandraEndpoint,
userContext?.databaseAccount?.id,
container,
tableQuery,
);
}
container.refreshAllDatabases();
setIsExecuting(false);
closeSidePanel();
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneStartMessage, startKey);
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(errorMessage);
setIsExecuting(false);
const addCollectionPaneFailedMessage = {
...addCollectionPaneStartMessage,
error: errorMessage,
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
};
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: t(Keys.common.ok),
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
return (
<RightPaneForm {...props}>
<div className="panelMainContent">
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.keyspaceLabel)}{" "}
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.keyspaceTooltip)}</InfoTooltip>
</Text>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="panelRadioBtn"
aria-label="Create new keyspace"
checked={keyspaceCreateNew}
type="radio"
role="radio"
tabIndex={0}
onChange={() => {
setKeyspaceCreateNew(true);
setIsKeyspaceShared(false);
setExistingKeyspaceId("");
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<input
className="panelRadioBtn"
aria-label="Use existing keyspace"
checked={!keyspaceCreateNew}
type="radio"
role="radio"
tabIndex={0}
onChange={() => {
setKeyspaceCreateNew(false);
setIsKeyspaceShared(false);
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
</Stack>
{keyspaceCreateNew && (
<Stack className="panelGroupSpacing">
<TextField
aria-required="true"
required={true}
autoComplete="off"
styles={getTextFieldStyles()}
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder="Type a new keyspace id"
size={40}
value={newKeyspaceId}
onChange={(e, newValue) => setNewKeyspaceId(newValue)}
ariaLabel="Keyspace id"
autoFocus
/>
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label="Provision shared throughput"
checked={isKeyspaceShared}
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => setIsKeyspaceShared(isChecked)}
/>
<InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within
the keyspace
</InfoTooltip>
</Stack>
)}
</Stack>
)}
{!keyspaceCreateNew && (
<Dropdown
ariaLabel="Choose existing keyspace id"
styles={{ root: { width: 300 }, title: { fontSize: 12 }, dropdownItem: { fontSize: 12 } }}
placeholder="Choose existing keyspace id"
defaultSelectedKey={existingKeyspaceId}
options={useDatabases.getState().databases?.map((keyspace) => ({
key: keyspace.id(),
text: keyspace.id(),
data: {
isShared: !!keyspace.offer(),
},
}))}
onChange={(event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) => {
setExistingKeyspaceId(option.key as string);
setIsKeyspaceShared(option.data.isShared);
}}
responsiveMode={999}
/>
)}
{!isServerlessAccount() && keyspaceCreateNew && isKeyspaceShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()
}
isDatabase
isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
</Stack>
<Stack>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.tableIdLabel)}{" "}
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
{t(Keys.common.learnMore)}
</Link>
</Text>
</Stack>
<Stack horizontal verticalAlign="center">
<Text variant="small" style={{ marginRight: 4 }}>
{`CREATE TABLE ${keyspaceCreateNew ? newKeyspaceId : existingKeyspaceId}.`}
</Text>
<TextField
underlined
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
aria-required="true"
required={true}
ariaLabel="addCollection-table Id Create table"
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.cassandraAddCollection.enterTableId)}
size={20}
value={tableId}
onChange={(e, newValue) => setTableId(newValue)}
/>
</Stack>
<TextField
styles={getTextFieldStyles()}
multiline
id="editor-area"
rows={5}
ariaLabel={t(Keys.panes.cassandraAddCollection.tableSchemaAriaLabel)}
value={userTableQuery}
onChange={(e, newValue) => setUserTableQuery(newValue)}
/>
</Stack>
{!isServerlessAccount() && isKeyspaceShared && !keyspaceCreateNew && (
<Stack horizontal verticalAlign="center">
<input
type="checkbox"
id="tableSharedThroughput"
title={t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}
checked={dedicateTableThroughput}
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
/>
<span>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}</span>
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughputTooltip)}</InfoTooltip>
</Stack>
)}
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
</div>
</RightPaneForm>
);
};