mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-03-29 05:00:20 +00:00
350 lines
14 KiB
TypeScript
350 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 * 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 { useSidePanel } from "hooks/useSidePanel";
|
|
import React, { FunctionComponent, useState } from "react";
|
|
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,
|
|
flight: userContext.addCollectionFlight,
|
|
},
|
|
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
|
|
? "Please acknowledge the estimated monthly spend."
|
|
: "Please acknowledge the estimated daily spend.";
|
|
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: "OK",
|
|
isSubmitButtonDisabled: isThroughputCapExceeded,
|
|
onSubmit,
|
|
};
|
|
|
|
return (
|
|
<RightPaneForm {...props}>
|
|
<div className="panelMainContent">
|
|
<Stack>
|
|
<Stack horizontal>
|
|
<span className="mandatoryStar">* </span>
|
|
<Text className="panelTextBold" variant="small">
|
|
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</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">Create new</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">Use existing</span>
|
|
</Stack>
|
|
|
|
{keyspaceCreateNew && (
|
|
<Stack className="panelGroupSpacing">
|
|
<TextField
|
|
aria-required="true"
|
|
required={true}
|
|
autoComplete="off"
|
|
styles={getTextFieldStyles()}
|
|
pattern="[^/?#\\]*[^/?# \\]"
|
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
|
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">* </span>
|
|
<Text className="panelTextBold" variant="small">
|
|
Enter CQL command to create the table.{" "}
|
|
<Link href="https://aka.ms/cassandra-create-table" target="_blank">
|
|
Learn More
|
|
</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="[^/?#\\]*[^/?# \\]"
|
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
|
placeholder="Enter table Id"
|
|
size={20}
|
|
value={tableId}
|
|
onChange={(e, newValue) => setTableId(newValue)}
|
|
/>
|
|
</Stack>
|
|
|
|
<TextField
|
|
styles={getTextFieldStyles()}
|
|
multiline
|
|
id="editor-area"
|
|
rows={5}
|
|
aria-label="Table Schema"
|
|
value={userTableQuery}
|
|
onChange={(e, newValue) => setUserTableQuery(newValue)}
|
|
/>
|
|
</Stack>
|
|
|
|
{!isServerlessAccount() && isKeyspaceShared && !keyspaceCreateNew && (
|
|
<Stack>
|
|
<input
|
|
type="checkbox"
|
|
id="tableSharedThroughput"
|
|
title="Provision dedicated throughput for this table"
|
|
checked={dedicateTableThroughput}
|
|
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
|
|
/>
|
|
<span>Provision dedicated throughput for this table</span>
|
|
<InfoTooltip>
|
|
You can optionally provision dedicated throughput for a table within a keyspace that has throughput
|
|
provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and
|
|
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
|
|
billed in addition to the throughput amount you provisioned at the keyspace level.
|
|
</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>
|
|
);
|
|
};
|